@objectstack/metadata-fs 17.1.0 → 17.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/dist/index.cjs +101 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +101 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
# @objectstack/metadata-fs
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e7191ce: fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
|
|
8
|
+
|
|
9
|
+
**Published-surface change, zero runtime change.** No emitted byte moves; what
|
|
10
|
+
moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
|
|
11
|
+
(decision batch #3, verbatim 「同意」) chose declaring the files over deleting
|
|
12
|
+
them.
|
|
13
|
+
|
|
14
|
+
## What was wrong
|
|
15
|
+
|
|
16
|
+
These 28 packages are `"type": "module"` and dual-built, and each spelled one
|
|
17
|
+
`types` condition as a **sibling** of `import`/`require`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
"exports": { ".": {
|
|
21
|
+
"types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
|
|
22
|
+
} }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A sibling `types` answers for **both** conditions, so a CommonJS consumer was
|
|
26
|
+
handed `dist/index.d.ts` — an ES-module declaration, because the package is
|
|
27
|
+
`"type": "module"` — for an entry point it reaches with `require`. Measured with
|
|
28
|
+
`tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
|
|
29
|
+
node16`:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
error TS1479: The current file is a CommonJS module whose imports will produce
|
|
33
|
+
'require' calls; however, the referenced file is an ECMAScript module and cannot
|
|
34
|
+
be imported with 'require'.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
|
|
38
|
+
has asserted that for months). It is the **types** that told the consumer the
|
|
39
|
+
supported `require` entry point could not be required. The `dist/index.d.cts`
|
|
40
|
+
twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
|
|
41
|
+
no condition at all and shipped in every tarball unreachable.
|
|
42
|
+
|
|
43
|
+
## What changed
|
|
44
|
+
|
|
45
|
+
Each condition now names its own declaration, the shape TypeScript documents:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
"exports": { ".": {
|
|
49
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
50
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
51
|
+
} }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
33 entry points across 27 packages, subpaths included. The root `types` field is
|
|
55
|
+
untouched, so `node10` resolvers are unaffected; the `import` condition resolves
|
|
56
|
+
exactly what it resolved before, measured as an unchanged control in the same
|
|
57
|
+
run.
|
|
58
|
+
|
|
59
|
+
## `@objectstack/core` is deliberately NOT changed
|
|
60
|
+
|
|
61
|
+
Splitting a declaration in two makes TypeScript compare it nominally, and
|
|
62
|
+
`ObjectKernel` carries a `private plugins` member that reaches every plugin
|
|
63
|
+
through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
|
|
64
|
+
fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
|
|
65
|
+
declarations of a private property 'plugins'"); with core held back and the
|
|
66
|
+
other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
|
|
67
|
+
its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
|
|
68
|
+
`check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
|
|
69
|
+
types, not about an exports map.
|
|
70
|
+
|
|
71
|
+
## For consumers
|
|
72
|
+
|
|
73
|
+
- **ESM consumers: nothing changes.** Same declaration file, byte for byte.
|
|
74
|
+
- **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
|
|
75
|
+
declarations they get are the ones built for CommonJS.
|
|
76
|
+
- **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
|
|
77
|
+
read `exports`.
|
|
78
|
+
- Nothing is removed: every path that resolved before still resolves.
|
|
79
|
+
|
|
80
|
+
Packages that are CJS-first (`require` → `./dist/index.js`, no `"type": "module"`)
|
|
81
|
+
were already correct and are untouched — their `dist/index.d.ts` really is the
|
|
82
|
+
CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
|
|
83
|
+
`import` condition) is a separate, larger population and is filed separately per
|
|
84
|
+
the ruling, not fixed here.
|
|
85
|
+
|
|
86
|
+
`check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
|
|
87
|
+
shape, so the drift cannot return silently.
|
|
88
|
+
- a07a831: fix(metadata-fs): declare `startWatcher()`'s chokidar `atomic` option explicitly (#12696)
|
|
89
|
+
|
|
90
|
+
`FileSystemRepository.startWatcher()` constructed its chokidar watcher with
|
|
91
|
+
`usePolling: true` but never passed `atomic`, leaving it to inherit chokidar's
|
|
92
|
+
default. That default is unconditionally `true` in the installed version
|
|
93
|
+
(chokidar 5.0.0): the defaults literal assigns `atomic: true` *before* the
|
|
94
|
+
caller's options are spread in, so chokidar's own default-correction
|
|
95
|
+
(`if (opts.atomic === undefined) opts.atomic = !opts.usePolling`) can never
|
|
96
|
+
fire — it only runs when `atomic` is literally `undefined` after the merge,
|
|
97
|
+
which it never is. The comment beside that correction ("Editor atomic write
|
|
98
|
+
normalization enabled by default with fs.watch") reads as "off under
|
|
99
|
+
polling"; the actual resolved behaviour was on regardless.
|
|
100
|
+
|
|
101
|
+
This change passes `atomic: true` explicitly at the call site, with a comment
|
|
102
|
+
explaining why. **Patch, not a behaviour change**: verified at runtime
|
|
103
|
+
(constructing a watcher the way `startWatcher()` does and reading back
|
|
104
|
+
`watcher.options.atomic`) that the resolved value is identical before and
|
|
105
|
+
after — `true` either way, today. The only thing that changes is that the
|
|
106
|
+
value is now DECLARED rather than inherited from an upstream branch that
|
|
107
|
+
cannot execute, so a future chokidar release that fixes the ordering (making
|
|
108
|
+
the correction real) cannot silently flip this repository's watcher to
|
|
109
|
+
`atomic: false` under polling and change behaviour with no diff to review.
|
|
110
|
+
|
|
111
|
+
Not addressed here (see #12696): whether `atomic: true` (the 100ms
|
|
112
|
+
unlink-coalescing deferral and the `DOT_RE` editor-temp-file matcher it turns
|
|
113
|
+
on) is actually the right value. No evidence surfaced that either has ever
|
|
114
|
+
affected a run; flipping it to `false` is a deliberate behaviour change to a
|
|
115
|
+
live delivery path that needs its own reverse verification, and is out of
|
|
116
|
+
scope for this card.
|
|
117
|
+
- 3e8f5b0: The file watcher no longer publishes a `delete` for an item that is still on disk.
|
|
118
|
+
|
|
119
|
+
A watcher `unlink` is a claim of absence, not absence: chokidar reaches its removal path from failed stats as well as from real removals, so under filesystem pressure it can retire a file that is still there. `FileSystemRepository` published those claims straight through as `delete` events — appended to the change log and broadcast to every subscriber, which drops the item from the metadata registry and the `list()` cache — and the reconciliation sweep then republished the untouched file as a `create`. A failed stat therefore produced a durable delete/create pair for an item nobody removed, with a window in between where live metadata had disappeared.
|
|
120
|
+
|
|
121
|
+
The removal face now confirms the absence against the disk under the same per-key lock the reconciliation sweep already used for this, and an `unlink` for a path that still exists falls through to the content comparison — so a spurious unlink that arrived alongside a real external edit surfaces as the `update` it always was. Genuine external removals are unaffected and are still published on the first delivery.
|
|
122
|
+
- Updated dependencies [54e2d36]
|
|
123
|
+
- Updated dependencies [b745157]
|
|
124
|
+
- Updated dependencies [d23ebb9]
|
|
125
|
+
- Updated dependencies [fa5d137]
|
|
126
|
+
- Updated dependencies [e7191ce]
|
|
127
|
+
- Updated dependencies [0fd4899]
|
|
128
|
+
- Updated dependencies [00d8f65]
|
|
129
|
+
- Updated dependencies [200d255]
|
|
130
|
+
- Updated dependencies [2852acc]
|
|
131
|
+
- Updated dependencies [15d55fb]
|
|
132
|
+
- Updated dependencies [15eb2c9]
|
|
133
|
+
- Updated dependencies [1272f0a]
|
|
134
|
+
- Updated dependencies [d41d166]
|
|
135
|
+
- Updated dependencies [5d16379]
|
|
136
|
+
- @objectstack/metadata-core@17.3.0
|
|
137
|
+
|
|
138
|
+
## 17.2.0
|
|
139
|
+
|
|
140
|
+
### Patch Changes
|
|
141
|
+
|
|
142
|
+
- 46644e2: `FileSystemRepository.close()` now terminates every live `watch()` iterator
|
|
143
|
+
instead of leaving it parked (#11127). A consumer holding a `for await` over
|
|
144
|
+
`watch()` at shutdown never saw its loop end — on a repository that was already
|
|
145
|
+
gone.
|
|
146
|
+
|
|
147
|
+
`close()` retired the chokidar watcher and the resync sweep and stopped there.
|
|
148
|
+
It never reached the event broker, and the broker had no teardown of its own:
|
|
149
|
+
`subscribe`/`unsubscribe` add to and delete from a plain `Set`, and nothing else
|
|
150
|
+
emptied it. Each iterator parks its pending `next()` on a `waiter` that only two
|
|
151
|
+
things can settle — a broker `push`, or the iterator's own terminator, which ran
|
|
152
|
+
from `iterator.return()`/`throw()` and from nowhere else. After `close()` the
|
|
153
|
+
chokidar source was gone so no `push` could arrive, and the subscriber was still
|
|
154
|
+
registered with nothing left to run its terminator.
|
|
155
|
+
|
|
156
|
+
Unlike the sibling defect in `SysMetadataRepository` (#11021) this was not
|
|
157
|
+
filter-dependent: there was no drain attempt at all, so every subscription shape
|
|
158
|
+
hung, `watch({})` included. Measured before the fix: nine cases —
|
|
159
|
+
`watch({org}, seq)`, `watch({org})`, `watch({})`, a ref-exact filter, a watcher
|
|
160
|
+
over the real chokidar watcher, a watcher with no pull outstanding, four
|
|
161
|
+
concurrent watchers, and the `return()`-symmetry comparison — were all still
|
|
162
|
+
unsettled 2s after `close()`. `MetadataManager.startRepositoryWatch()`, which
|
|
163
|
+
awaits `iter.next()` in a loop, is exactly the shape that hung.
|
|
164
|
+
|
|
165
|
+
The broker now holds each subscription's terminator next to its event sink, and
|
|
166
|
+
`close()` runs every terminator — the same routine the consumer's own
|
|
167
|
+
`iterator.return()` runs, so a parked `next()` settles with `{ done: true }` and
|
|
168
|
+
no value, and so does every later one. Shutdown is deliberately **not** delivered
|
|
169
|
+
as an event: a synthetic drain event is subject to the very filters `watch()`
|
|
170
|
+
applies to real ones, and delivering an event has never ended an iterator
|
|
171
|
+
(invariant 8, `@objectstack/metadata-core`'s `repository.ts`).
|
|
172
|
+
|
|
173
|
+
One narrower path is closed with it. `watch()` returns a deferred iterable whose
|
|
174
|
+
subscriber registers only once the eager log read resolves, so a `close()`
|
|
175
|
+
landing inside that window swept a broker the subscription had not yet joined —
|
|
176
|
+
the same forever-parked shape by a different route. `watch()` now carries the
|
|
177
|
+
close generation it was opened under, and a subscription that arrives after a
|
|
178
|
+
shutdown terminates on arrival.
|
|
179
|
+
|
|
180
|
+
Invariant 8 named `FileSystemRepository` as its one known non-conformance. With
|
|
181
|
+
this change the invariant has no declared exceptions, and its text says so.
|
|
182
|
+
- Updated dependencies [8cc8401]
|
|
183
|
+
- Updated dependencies [26f3588]
|
|
184
|
+
- Updated dependencies [05bc692]
|
|
185
|
+
- Updated dependencies [f334d66]
|
|
186
|
+
- Updated dependencies [2810695]
|
|
187
|
+
- @objectstack/metadata-core@17.2.0
|
|
188
|
+
|
|
3
189
|
## 17.1.0
|
|
4
190
|
|
|
5
191
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -144,6 +144,16 @@ function createBroker(matches) {
|
|
|
144
144
|
if (!matches(evt, s.filter)) continue;
|
|
145
145
|
s.push(evt);
|
|
146
146
|
}
|
|
147
|
+
},
|
|
148
|
+
terminateAll: () => {
|
|
149
|
+
const snapshot = Array.from(subs);
|
|
150
|
+
subs.clear();
|
|
151
|
+
for (const s of snapshot) {
|
|
152
|
+
try {
|
|
153
|
+
s.terminate();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
147
157
|
}
|
|
148
158
|
};
|
|
149
159
|
}
|
|
@@ -158,6 +168,10 @@ function createWatchIterable(args) {
|
|
|
158
168
|
const subscriber = {
|
|
159
169
|
filter: args.filter,
|
|
160
170
|
closed: false,
|
|
171
|
+
// Assigned below, once `close` exists. Termination and the consumer's own
|
|
172
|
+
// `return()` are ONE routine, deliberately: invariant 8 requires shutdown
|
|
173
|
+
// to be indistinguishable from `iterator.return()`.
|
|
174
|
+
terminate: () => void 0,
|
|
161
175
|
push: (evt) => {
|
|
162
176
|
if (subscriber.closed) return;
|
|
163
177
|
const k = evtKey(evt);
|
|
@@ -206,6 +220,8 @@ function createWatchIterable(args) {
|
|
|
206
220
|
}
|
|
207
221
|
return { value: void 0, done: true };
|
|
208
222
|
};
|
|
223
|
+
subscriber.terminate = close;
|
|
224
|
+
if (args.arrivesClosed?.()) close();
|
|
209
225
|
const iterator = {
|
|
210
226
|
next: () => {
|
|
211
227
|
if (closed) return Promise.resolve({ value: void 0, done: true });
|
|
@@ -257,6 +273,15 @@ var FileSystemRepository = class {
|
|
|
257
273
|
* the first degradation). An entry is cleared when that path reads again.
|
|
258
274
|
*/
|
|
259
275
|
this.resyncFaults = /* @__PURE__ */ new Set();
|
|
276
|
+
/**
|
|
277
|
+
* Bumped by every `close()`. `watch()` reads it before its deferred log
|
|
278
|
+
* replay starts and hands the comparison to `createWatchIterable`, so a
|
|
279
|
+
* subscription that registers AFTER the shutdown sweep terminates on
|
|
280
|
+
* arrival instead of parking forever (#11127). A counter rather than a
|
|
281
|
+
* boolean because `start()` may follow `close()`: a repository restart must
|
|
282
|
+
* not poison the watchers opened after it.
|
|
283
|
+
*/
|
|
284
|
+
this.closeGeneration = 0;
|
|
260
285
|
this.org = opts.org;
|
|
261
286
|
this.fsActor = opts.fsActor ?? "fs";
|
|
262
287
|
this.disableWatch = opts.disableWatch ?? false;
|
|
@@ -302,8 +327,35 @@ var FileSystemRepository = class {
|
|
|
302
327
|
await import_promises2.default.mkdir(this.layout.root, { recursive: true });
|
|
303
328
|
if (this.started && !this.disableWatch && !this.watcher) this.startWatcher();
|
|
304
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Shut the repository down, ending every live `watch()` iterator.
|
|
332
|
+
*
|
|
333
|
+
* **Shutdown terminates; it does not emit** — invariant 8 in
|
|
334
|
+
* `@objectstack/metadata-core`'s `repository.ts`, and the reason this method
|
|
335
|
+
* reaches the broker at all. It used to retire the chokidar watcher and the
|
|
336
|
+
* resync sweep and stop there. The broker has no teardown of its own
|
|
337
|
+
* (`subscribe`/`unsubscribe` add to and delete from a plain `Set`), and each
|
|
338
|
+
* iterator parks its pending `next()` on a `waiter` that only a broker
|
|
339
|
+
* `push` or the iterator's own terminator can settle. After `close()` the
|
|
340
|
+
* chokidar source was gone, so no `push` could arrive; the subscriber was
|
|
341
|
+
* still registered, and nothing ran its terminator. A consumer holding a
|
|
342
|
+
* `for await` at shutdown — `MetadataManager.startRepositoryWatch()` is
|
|
343
|
+
* exactly that shape — therefore never saw its loop end, for EVERY
|
|
344
|
+
* subscription shape including `watch({})`.
|
|
345
|
+
*
|
|
346
|
+
* Termination is expressed as termination: each subscription's
|
|
347
|
+
* `terminate()`, which is the same routine the consumer's own
|
|
348
|
+
* `iterator.return()` runs, so no consumer has to tell "the repository shut
|
|
349
|
+
* down under me" apart from "I broke my own loop". A synthetic drain event
|
|
350
|
+
* would be the wrong shape and was measured to be so (#11021): the
|
|
351
|
+
* subscriptions most in need of draining are exactly the ones whose filter
|
|
352
|
+
* or numeric `since` drops it, and delivering an event has never ended an
|
|
353
|
+
* iterator.
|
|
354
|
+
*/
|
|
305
355
|
async close() {
|
|
306
356
|
this.stopResync();
|
|
357
|
+
this.closeGeneration++;
|
|
358
|
+
this.broker.terminateAll();
|
|
307
359
|
if (this.watcher) {
|
|
308
360
|
await this.watcher.close();
|
|
309
361
|
this.watcher = null;
|
|
@@ -378,6 +430,7 @@ var FileSystemRepository = class {
|
|
|
378
430
|
if (matchEvent(evt, filter)) replay.push(evt);
|
|
379
431
|
}
|
|
380
432
|
})();
|
|
433
|
+
const generation = this.closeGeneration;
|
|
381
434
|
return deferredIterable(promise.then(
|
|
382
435
|
() => createWatchIterable({
|
|
383
436
|
filter,
|
|
@@ -385,7 +438,8 @@ var FileSystemRepository = class {
|
|
|
385
438
|
replay,
|
|
386
439
|
broker: this.broker,
|
|
387
440
|
matches: matchEvent,
|
|
388
|
-
branchKeyOf: (e) => e.ref.org
|
|
441
|
+
branchKeyOf: (e) => e.ref.org,
|
|
442
|
+
arrivesClosed: () => this.closeGeneration !== generation
|
|
389
443
|
})
|
|
390
444
|
));
|
|
391
445
|
}
|
|
@@ -598,7 +652,24 @@ var FileSystemRepository = class {
|
|
|
598
652
|
// the entire customization tree.
|
|
599
653
|
usePolling: true,
|
|
600
654
|
interval: 1e3,
|
|
601
|
-
binaryInterval: 2e3
|
|
655
|
+
binaryInterval: 2e3,
|
|
656
|
+
// Declared explicitly, not inherited. chokidar's own default-correction
|
|
657
|
+
// (`if (opts.atomic === undefined) opts.atomic = !opts.usePolling`) can
|
|
658
|
+
// only fire when the caller omits `atomic`, but its defaults literal
|
|
659
|
+
// already assigns `atomic: true` *before* the caller's options are
|
|
660
|
+
// spread in — so leaving `atomic` unset here does not mean "off under
|
|
661
|
+
// polling" the way the correction's own comment claims, it silently
|
|
662
|
+
// resolves to `true` regardless of `usePolling`. That has been this
|
|
663
|
+
// repository's actual runtime behaviour all along (verified by reading
|
|
664
|
+
// back the resolved option from a real watcher instance, #12696): every
|
|
665
|
+
// `unlink` gets chokidar's 100ms editor-atomic-write deferral, and
|
|
666
|
+
// `DOT_RE` (vim swap files, `~`, sublime tmp) is folded into
|
|
667
|
+
// `_isIgnored` on top of this repository's own `isIgnoredWatchPath`
|
|
668
|
+
// (#7150). `atomic: true` here keeps that behaviour byte-for-byte —
|
|
669
|
+
// this is a declaration, not a change. Flipping it to `false` would
|
|
670
|
+
// remove both behaviours from a live delivery path and needs its own
|
|
671
|
+
// reverse verification; see #12696 for the analysis.
|
|
672
|
+
atomic: true
|
|
602
673
|
});
|
|
603
674
|
w.on("add", (p) => void this.handleFsChange(p, "add"));
|
|
604
675
|
w.on("change", (p) => void this.handleFsChange(p, "change"));
|
|
@@ -864,9 +935,36 @@ var FileSystemRepository = class {
|
|
|
864
935
|
* - `unlink` — `!currentHead` drops the event when the index already
|
|
865
936
|
* agrees the item is gone. `delete()` retires the head *before* it
|
|
866
937
|
* unlinks, precisely because this face gets no `awaitWriteFinish` delay.
|
|
938
|
+
* This is the whole of the *self-write* answer on this face, and it is
|
|
939
|
+
* not the whole of the face — see the section below it.
|
|
867
940
|
*
|
|
868
941
|
* Both faces are pinned together in `test/self-write-suppression.test.ts`.
|
|
869
942
|
*
|
|
943
|
+
* ## A removal is confirmed against the disk before it is published (#7369)
|
|
944
|
+
*
|
|
945
|
+
* Those two checks answer "is this event OURS". Neither answers "did this
|
|
946
|
+
* happen at all", and the `unlink` face needs that second question asked
|
|
947
|
+
* because its input is a third party's inference: chokidar decides a file is
|
|
948
|
+
* gone from a *stat that failed*, not only from a file that went away, and
|
|
949
|
+
* `!currentHead` cannot tell the two apart because a spurious unlink leaves
|
|
950
|
+
* the index exactly as valid as it was.
|
|
951
|
+
*
|
|
952
|
+
* The cost of getting it wrong is not a dropped notification, which the
|
|
953
|
+
* sweep would repair. A `delete` is appended to the change log and broadcast
|
|
954
|
+
* to every subscriber, and `MetadataManager` drops the item from the
|
|
955
|
+
* registry and the `list()` cache on receipt. The sweep then finds the file
|
|
956
|
+
* still on disk and republishes it as a `create` — so a failed stat becomes
|
|
957
|
+
* a durable, permanently recorded delete/create pair for an item that never
|
|
958
|
+
* changed, and every consumer sees the item disappear in between. That is
|
|
959
|
+
* the shape ADR-0008's log is least able to walk back.
|
|
960
|
+
*
|
|
961
|
+
* So `existsSync` under the same per-key lock the sweep uses, and the same
|
|
962
|
+
* decision it makes: absent ⇒ publish the removal; present ⇒ this was a
|
|
963
|
+
* change, answered by the content path below. Genuine removals pay nothing —
|
|
964
|
+
* `delete()` is still suppressed by `!currentHead`, and an external `rm` is
|
|
965
|
+
* still published on the first delivery, because for those the file really
|
|
966
|
+
* is gone. Pinned in `test/external-delete-requires-absence.test.ts`.
|
|
967
|
+
*
|
|
870
968
|
* Note the deliberate limit: identity is judged on what round-trips through
|
|
871
969
|
* the file, so a spec whose in-memory form does not (a `Date`, which
|
|
872
970
|
* canonicalises to `{}` in memory but to an ISO string once written and
|
|
@@ -885,7 +983,7 @@ var FileSystemRepository = class {
|
|
|
885
983
|
};
|
|
886
984
|
const key = (0, import_metadata_core.refKey)(ref);
|
|
887
985
|
await this.mutex.run(key, async () => {
|
|
888
|
-
if (kind === "unlink") {
|
|
986
|
+
if (kind === "unlink" && !(0, import_node_fs2.existsSync)(absPath)) {
|
|
889
987
|
await this.publishExternalDelete(ref, key);
|
|
890
988
|
return;
|
|
891
989
|
}
|