codekingpro/portable-devtools
114k
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.
8import { join } from 'node:path';
9import { normalizeUnicode } from './normalize-unicode.js';
10import { stripTrailingSlashes } from './strip-trailing-slashes.js';
11const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
12const 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']
15const 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};
29export 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)));
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