103316c81d65af2c2a6ea8f9d5cb355781a6e79cb777226ce251d78a3af14ef7e739c4e8970dcc8985cf1529305442e3fb46bb7f3cb8dd37303956a4066161 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // A path exclusive reservation system
  2. // reserve([list, of, paths], fn)
  3. // When the fn is first in line for all its paths, it
  4. // is called with a cb that clears the reservation.
  5. //
  6. // Used by async unpack to avoid clobbering paths in use,
  7. // while still allowing maximal safe parallelization.
  8. import { join } from 'node:path';
  9. import { normalizeUnicode } from './normalize-unicode.js';
  10. import { stripTrailingSlashes } from './strip-trailing-slashes.js';
  11. const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
  12. const isWindows = platform === 'win32';
  13. // return a set of parent dirs for a given path
  14. // '/a/b/c/d' -> ['/', '/a', '/a/b', '/a/b/c', '/a/b/c/d']
  15. const getDirs = (path) => {
  16. const dirs = path
  17. .split('/')
  18. .slice(0, -1)
  19. .reduce((set, path) => {
  20. const s = set[set.length - 1];
  21. if (s !== undefined) {
  22. path = join(s, path);
  23. }
  24. set.push(path || '/');
  25. return set;
  26. }, []);
  27. return dirs;
  28. };
  29. export class PathReservations {
  30. // path => [function or Set]
  31. // A Set object means a directory reservation
  32. // A fn is a direct reservation on that path
  33. #queues = new Map();
  34. // fn => {paths:[path,...], dirs:[path, ...]}
  35. #reservations = new Map();
  36. // functions currently running
  37. #running = new Set();
  38. reserve(paths, fn) {
  39. paths =
  40. isWindows ?
  41. ['win32 parallelization disabled']
  42. : paths.map(p => {
  43. // don't need normPath, because we skip this entirely for windows
  44. return stripTrailingSlashes(join(normalizeUnicode(p))).toLowerCase();
  45. });
  46. const dirs = new Set(paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b)));
  47. this.#reservations.set(fn, { dirs, paths });
  48. for (const p of paths) {
  49. const q = this.#queues.get(p);
  50. if (!q) {
  51. this.#queues.set(p, [fn]);
  52. }
  53. else {
  54. q.push(fn);
  55. }
  56. }
  57. for (const dir of dirs) {
  58. const q = this.#queues.get(dir);
  59. if (!q) {
  60. this.#queues.set(dir, [new Set([fn])]);
  61. }
  62. else {
  63. const l = q[q.length - 1];
  64. if (l instanceof Set) {
  65. l.add(fn);
  66. }
  67. else {
  68. q.push(new Set([fn]));
  69. }
  70. }
  71. }
  72. return this.#run(fn);
  73. }
  74. // return the queues for each path the function cares about
  75. // fn => {paths, dirs}
  76. #getQueues(fn) {
  77. const res = this.#reservations.get(fn);
  78. /* c8 ignore start */
  79. if (!res) {
  80. throw new Error('function does not have any path reservations');
  81. }
  82. /* c8 ignore stop */
  83. return {
  84. paths: res.paths.map((path) => this.#queues.get(path)),
  85. dirs: [...res.dirs].map(path => this.#queues.get(path)),
  86. };
  87. }
  88. // check if fn is first in line for all its paths, and is
  89. // included in the first set for all its dir queues
  90. check(fn) {
  91. const { paths, dirs } = this.#getQueues(fn);
  92. return (paths.every(q => q && q[0] === fn) &&
  93. dirs.every(q => q && q[0] instanceof Set && q[0].has(fn)));
  94. }
  95. // run the function if it's first in line and not already running
  96. #run(fn) {
  97. if (this.#running.has(fn) || !this.check(fn)) {
  98. return false;
  99. }
  100. this.#running.add(fn);
  101. fn(() => this.#clear(fn));
  102. return true;
  103. }
  104. #clear(fn) {
  105. if (!this.#running.has(fn)) {
  106. return false;
  107. }
  108. const res = this.#reservations.get(fn);
  109. /* c8 ignore start */
  110. if (!res) {
  111. throw new Error('invalid reservation');
  112. }
  113. /* c8 ignore stop */
  114. const { paths, dirs } = res;
  115. const next = new Set();
  116. for (const path of paths) {
  117. const q = this.#queues.get(path);
  118. /* c8 ignore start */
  119. if (!q || q?.[0] !== fn) {
  120. continue;
  121. }
  122. /* c8 ignore stop */
  123. const q0 = q[1];
  124. if (!q0) {
  125. this.#queues.delete(path);
  126. continue;
  127. }
  128. q.shift();
  129. if (typeof q0 === 'function') {
  130. next.add(q0);
  131. }
  132. else {
  133. for (const f of q0) {
  134. next.add(f);
  135. }
  136. }
  137. }
  138. for (const dir of dirs) {
  139. const q = this.#queues.get(dir);
  140. const q0 = q?.[0];
  141. /* c8 ignore next - type safety only */
  142. if (!q || !(q0 instanceof Set))
  143. continue;
  144. if (q0.size === 1 && q.length === 1) {
  145. this.#queues.delete(dir);
  146. continue;
  147. }
  148. else if (q0.size === 1) {
  149. q.shift();
  150. // next one must be a function,
  151. // or else the Set would've been reused
  152. const n = q[0];
  153. if (typeof n === 'function') {
  154. next.add(n);
  155. }
  156. }
  157. else {
  158. q0.delete(fn);
  159. }
  160. }
  161. this.#running.delete(fn);
  162. next.forEach(fn => this.#run(fn));
  163. return true;
  164. }
  165. }
  166. //# sourceMappingURL=path-reservations.js.map