@hachej/boring-files 0.2.0 → 0.2.2
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/README.md +21 -1
- package/dist/contracts.d.ts +8 -0
- package/dist/file-limits.d.ts +10 -0
- package/dist/file-limits.js +16 -0
- package/dist/file-types.d.ts +30 -0
- package/dist/file-types.js +61 -0
- package/dist/journal.d.ts +7 -0
- package/dist/journal.js +11 -3
- package/dist/publication-input.js +6 -0
- package/dist/revision-contracts.d.ts +41 -0
- package/dist/revision-handler.d.ts +3 -0
- package/dist/revision-handler.js +109 -1
- package/dist/revision-protocol.d.ts +8 -1
- package/dist/revision-protocol.js +65 -1
- package/dist/revision.js +89 -1
- package/dist/sqlite-batch.d.ts +3 -0
- package/dist/sqlite-filesystem.d.ts +2 -0
- package/dist/sqlite-filesystem.js +75 -12
- package/dist/workspace-catalog.js +22 -9
- package/dist/workspace.d.ts +66 -8
- package/dist/workspace.js +268 -23
- package/package.json +8 -4
package/dist/workspace.js
CHANGED
|
@@ -2,7 +2,10 @@ import { createWorkspaceCatalog } from './workspace-catalog.js';
|
|
|
2
2
|
import { committedChanges } from './journal.js';
|
|
3
3
|
import { randomUUID, sha1 } from './platform.js';
|
|
4
4
|
import { accessSnapshot, identifier, locator, publicationDigest, publicationSnapshot } from './publication-input.js';
|
|
5
|
+
import { assertLimit, fileTooLarge } from './file-limits.js';
|
|
5
6
|
import { journalConnectionOf, sqliteBatchOf } from './sqlite-batch.js';
|
|
7
|
+
import { isTemporary, mediaTypeOf, temporaryFileName } from './file-types.js';
|
|
8
|
+
export { isTemporary } from './file-types.js';
|
|
6
9
|
const sameBytes = (left, right) => left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
7
10
|
const queues = new WeakMap();
|
|
8
11
|
/** One queue per file system object: two providers over the same `fs` share it. */
|
|
@@ -30,16 +33,6 @@ export async function blobRevision(bytes) {
|
|
|
30
33
|
whole.set(bytes, header.length);
|
|
31
34
|
return hex(await sha1(whole));
|
|
32
35
|
}
|
|
33
|
-
const mediaTypes = {
|
|
34
|
-
md: 'text/markdown', markdown: 'text/markdown', html: 'text/html', htm: 'text/html', json: 'application/json', svg: 'image/svg+xml', txt: 'text/plain', tldraw: 'application/vnd.tldraw+json',
|
|
35
|
-
};
|
|
36
|
-
function mediaTypeOf(path) {
|
|
37
|
-
return mediaTypes[path.slice(path.lastIndexOf('.') + 1).toLowerCase()] ?? 'application/octet-stream';
|
|
38
|
-
}
|
|
39
|
-
/** The provider's own temporary files. They are removed when the provider opens and are never a resource. */
|
|
40
|
-
const temporaryPattern = /^\.boring-[0-9a-f-]{36}\.tmp$/;
|
|
41
|
-
/** Whether a path is one of the provider's temporary files (a lister hides them). */
|
|
42
|
-
export const isTemporary = (path) => temporaryPattern.test(path.slice(path.lastIndexOf('/') + 1));
|
|
43
36
|
export function createWorkspaceProvider(options) {
|
|
44
37
|
const { fs, journal } = options;
|
|
45
38
|
const identity = options.identity;
|
|
@@ -58,6 +51,11 @@ export function createWorkspaceProvider(options) {
|
|
|
58
51
|
const queue = queueFor(fs);
|
|
59
52
|
/** Present when `fs` is the SQLite file system (`@hachej/boring-files/sqlite-filesystem`): multi-file batches then commit in one transaction. */
|
|
60
53
|
const batch = sqliteBatchOf(fs);
|
|
54
|
+
if (options.maxFileBytes !== undefined) {
|
|
55
|
+
assertLimit(options.maxFileBytes);
|
|
56
|
+
batch?.setMaxFileBytes(options.maxFileBytes);
|
|
57
|
+
}
|
|
58
|
+
const limitOf = () => batch ? batch.maxFileBytes() : options.maxFileBytes;
|
|
61
59
|
const historyScope = JSON.stringify([key.providerId, key.instanceId, key.incarnation, key.viewId]);
|
|
62
60
|
const listeners = new Set();
|
|
63
61
|
const known = new Map();
|
|
@@ -71,14 +69,38 @@ export function createWorkspaceProvider(options) {
|
|
|
71
69
|
principal: access.principalId, initiator: access.initiatorId, operation: operationId,
|
|
72
70
|
});
|
|
73
71
|
const ownEvidence = (evidence) => evidence.startsWith(`${key.incarnation}:`);
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// ---- The change feed: a bounded, seq-ordered buffer of invalidation hints ----------------------------------------------------
|
|
73
|
+
const bufferLimit = options.changeBuffer ?? 1024;
|
|
74
|
+
if (!Number.isSafeInteger(bufferLimit) || bufferLimit < 1)
|
|
75
|
+
throw new RangeError('The change buffer must be a positive integer');
|
|
76
|
+
const lingerMs = options.watchLingerMs ?? 30_000;
|
|
77
|
+
// Sequence numbers start from the clock (microseconds), so a cursor from an earlier provider lifetime is older than this one's
|
|
78
|
+
// first event and reads as a gap, never as a position in this lifetime.
|
|
79
|
+
const firstSeq = Date.now() * 1000;
|
|
80
|
+
let head = firstSeq;
|
|
81
|
+
// Names this provider lifetime. A cursor from another lifetime is refused by this, not only by the clock: the clock may go back.
|
|
82
|
+
const incarnationOfFeed = randomUUID();
|
|
83
|
+
const lifetime = new AbortController();
|
|
84
|
+
const buffer = [];
|
|
85
|
+
const waiting = new Set();
|
|
86
|
+
function append(event) {
|
|
87
|
+
head += 1;
|
|
88
|
+
buffer.push(Object.freeze({ ...event, seq: head, at: Date.now() }));
|
|
89
|
+
if (buffer.length > bufferLimit)
|
|
90
|
+
buffer.shift();
|
|
91
|
+
for (const wake of [...waiting])
|
|
92
|
+
wake();
|
|
93
|
+
}
|
|
94
|
+
function emit(change, extra = {}) {
|
|
95
|
+
if (!extra.directory)
|
|
96
|
+
known.set(change.path, change.revision);
|
|
76
97
|
for (const listener of [...listeners]) {
|
|
77
98
|
try {
|
|
78
99
|
listener(change);
|
|
79
100
|
}
|
|
80
101
|
catch { /* a listener failure cannot undo a write */ }
|
|
81
102
|
}
|
|
103
|
+
append({ kind: 'change', ...change, ...extra });
|
|
82
104
|
}
|
|
83
105
|
const dirname = (path) => path.slice(0, path.lastIndexOf('/')) || '/';
|
|
84
106
|
const inside = (base, path) => path === base || path.startsWith(base === '/' ? '/' : `${base}/`);
|
|
@@ -182,7 +204,7 @@ export function createWorkspaceProvider(options) {
|
|
|
182
204
|
const result = await read({ target: { resource: { providerId, path }, view: { kind: 'published' } }, revision: { kind: 'latest' } }, access);
|
|
183
205
|
if (result.kind === 'available') {
|
|
184
206
|
const path = result.snapshot.ref.resource.path;
|
|
185
|
-
journal.remember(historyScope, path, { revision: result.snapshot.ref.revision, bytes: result.snapshot.bytes, mediaType: result.snapshot.mediaType, savedAt: await savedAtOf(path) });
|
|
207
|
+
journal.remember(historyScope, path, { revision: result.snapshot.ref.revision, bytes: result.snapshot.bytes, mediaType: result.snapshot.mediaType, savedAt: await savedAtOf(path), source: 'observed' });
|
|
186
208
|
}
|
|
187
209
|
return result;
|
|
188
210
|
});
|
|
@@ -228,6 +250,15 @@ export function createWorkspaceProvider(options) {
|
|
|
228
250
|
}
|
|
229
251
|
if (journal.intent(operation))
|
|
230
252
|
return { kind: 'unknown', operationId: request.operationId, reason: 'An earlier attempt of this operation began and has no recorded outcome' };
|
|
253
|
+
// After the receipt and intent checks, so a replay of a committed operation keeps its result under a lower limit.
|
|
254
|
+
const limit = limitOf();
|
|
255
|
+
if (limit !== undefined) {
|
|
256
|
+
const oversized = changes.find(change => change.bytes.length > limit);
|
|
257
|
+
if (oversized) {
|
|
258
|
+
const size = oversized.bytes.length, path = oversized.target.resource.path;
|
|
259
|
+
return { kind: 'file_too_large', path, size, limit, reason: fileTooLarge(path, size, limit).message };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
231
262
|
journal.begin(operation, digest);
|
|
232
263
|
let applying = false;
|
|
233
264
|
const refuse = (reason) => { journal.cancel(operation); return { kind: 'unavailable', reason }; };
|
|
@@ -276,11 +307,11 @@ export function createWorkspaceProvider(options) {
|
|
|
276
307
|
}
|
|
277
308
|
const evidence = `${key.incarnation}:${randomUUID()}`;
|
|
278
309
|
const versions = plans.flatMap(({ change, path, before, after, unchanged, beforeSavedAt }) => {
|
|
279
|
-
const kept = (revision, bytes, mediaType, savedAt) => ({ scope: historyScope, path, version: { revision, bytes, mediaType, savedAt } });
|
|
310
|
+
const kept = (revision, bytes, mediaType, savedAt, source) => ({ scope: historyScope, path, version: { revision, bytes, mediaType, savedAt, source } });
|
|
280
311
|
return unchanged ? [] : [
|
|
281
312
|
...(change.kind === 'replace' && before?.kind === 'file'
|
|
282
|
-
? [kept(before.revision, before.bytes, journal.version(historyScope, path, before.revision)?.mediaType ?? mediaTypeOf(path), beforeSavedAt)] : []),
|
|
283
|
-
kept(after, change.bytes, change.mediaType, Date.now()),
|
|
313
|
+
? [kept(before.revision, before.bytes, journal.version(historyScope, path, before.revision)?.mediaType ?? mediaTypeOf(path), beforeSavedAt, 'observed')] : []),
|
|
314
|
+
kept(after, change.bytes, change.mediaType, Date.now(), 'publish'),
|
|
284
315
|
];
|
|
285
316
|
});
|
|
286
317
|
const record = {
|
|
@@ -321,7 +352,7 @@ export function createWorkspaceProvider(options) {
|
|
|
321
352
|
if (!unchanged) {
|
|
322
353
|
applying = true;
|
|
323
354
|
const target = absolute(path);
|
|
324
|
-
const temporary = `${target.slice(0, target.lastIndexOf('/'))}
|
|
355
|
+
const temporary = `${target.slice(0, target.lastIndexOf('/'))}/${temporaryFileName(randomUUID())}`;
|
|
325
356
|
const ctx = fsContext(access.signal);
|
|
326
357
|
const failure = async (reason) => { await fs.remove(temporary, { force: true }, fsContext()); return refuse(reason); };
|
|
327
358
|
if (change.kind === 'create') {
|
|
@@ -343,7 +374,7 @@ export function createWorkspaceProvider(options) {
|
|
|
343
374
|
}
|
|
344
375
|
for (const { path, after, unchanged } of plans)
|
|
345
376
|
if (!unchanged)
|
|
346
|
-
emit({ path, revision: after, source: '
|
|
377
|
+
emit({ path, revision: after, source: 'publish' });
|
|
347
378
|
return { kind: 'committed', receipt: receipt(request.operationId, access, record) };
|
|
348
379
|
}
|
|
349
380
|
catch (error) {
|
|
@@ -369,20 +400,63 @@ export function createWorkspaceProvider(options) {
|
|
|
369
400
|
? { kind: 'unknown', operationId: id, reason: 'The operation began and has no recorded outcome' }
|
|
370
401
|
: { kind: 'not-found' };
|
|
371
402
|
};
|
|
403
|
+
/** Every file of the workspace, relative to the root (not `.git`, `node_modules` or the provider's temporary files). */
|
|
404
|
+
async function everyFile(signal) {
|
|
405
|
+
const found = [];
|
|
406
|
+
let scanned = 0;
|
|
407
|
+
const walk = async (directory) => {
|
|
408
|
+
const listing = await fs.listDir(directory ? absolute(directory) : root, fsContext(signal));
|
|
409
|
+
if (!listing.ok)
|
|
410
|
+
return true;
|
|
411
|
+
for (const entry of listing.value) {
|
|
412
|
+
if (++scanned > 100_000)
|
|
413
|
+
return false;
|
|
414
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
415
|
+
if (entry.kind === 'directory') {
|
|
416
|
+
if (entry.name !== '.git' && entry.name !== 'node_modules' && !await walk(path))
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
else if (entry.kind === 'file' && !isTemporary(path))
|
|
420
|
+
found.push(path);
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
};
|
|
424
|
+
return await walk('') ? found : undefined;
|
|
425
|
+
}
|
|
426
|
+
let baselined = false;
|
|
372
427
|
const poll = async (paths, signal) => {
|
|
373
428
|
const changed = [];
|
|
374
429
|
await sweep();
|
|
375
|
-
|
|
430
|
+
let inputs;
|
|
431
|
+
let report = (path) => known.has(path);
|
|
432
|
+
if (paths === undefined) {
|
|
433
|
+
const files = await everyFile(signal);
|
|
434
|
+
// Too many files to hash on each turn: say so to every listener instead of reporting a partial picture.
|
|
435
|
+
if (files === undefined) {
|
|
436
|
+
append({ kind: 'resnapshot' });
|
|
437
|
+
return changed;
|
|
438
|
+
}
|
|
439
|
+
const present = new Set(files);
|
|
440
|
+
const removed = [...known].filter(([path, revision]) => revision !== null && !present.has(path)).map(([path]) => path);
|
|
441
|
+
inputs = [...files, ...removed];
|
|
442
|
+
if (baselined)
|
|
443
|
+
report = () => true;
|
|
444
|
+
baselined = true;
|
|
445
|
+
}
|
|
446
|
+
else
|
|
447
|
+
inputs = paths;
|
|
448
|
+
for (const input of inputs) {
|
|
376
449
|
const path = locator({ resource: { providerId, path: input }, view: { kind: 'published' } }).resource.path;
|
|
377
450
|
const now = await observe(path, signal);
|
|
378
451
|
if (now.kind === 'error' || now.kind === 'outside' || isTemporary(path))
|
|
379
452
|
continue;
|
|
380
453
|
const revision = now.kind === 'file' ? now.revision : null;
|
|
381
|
-
|
|
382
|
-
|
|
454
|
+
if (known.has(path) && known.get(path) === revision)
|
|
455
|
+
continue;
|
|
456
|
+
if (!known.has(path) && revision === null && paths === undefined)
|
|
383
457
|
continue;
|
|
384
458
|
const change = { path, revision, source: 'poll' };
|
|
385
|
-
if (
|
|
459
|
+
if (!report(path))
|
|
386
460
|
known.set(path, revision);
|
|
387
461
|
else {
|
|
388
462
|
emit(change);
|
|
@@ -391,10 +465,181 @@ export function createWorkspaceProvider(options) {
|
|
|
391
465
|
}
|
|
392
466
|
return changed;
|
|
393
467
|
};
|
|
468
|
+
const record = async (write, context) => {
|
|
469
|
+
const access = accessSnapshot(context);
|
|
470
|
+
if (write.source !== 'agent')
|
|
471
|
+
throw new TypeError('Only agent writes are recorded');
|
|
472
|
+
const path = locator({ resource: { providerId, path: write.path }, view: { kind: 'published' } }).resource.path;
|
|
473
|
+
const after = identifier(write.after);
|
|
474
|
+
const conversationId = write.conversationId === undefined ? undefined : identifier(write.conversationId);
|
|
475
|
+
// Known before the await: a watcher event for this same write, handled meanwhile, then finds nothing new and emits nothing twice.
|
|
476
|
+
if (!isTemporary(path))
|
|
477
|
+
known.set(path, after);
|
|
478
|
+
// Deliberately not `queue.run`: the file guard calls this from inside the queue, and the queue runs one piece at a time.
|
|
479
|
+
const now = await observe(path, access.signal);
|
|
480
|
+
if (now.kind === 'outside' || now.kind === 'error')
|
|
481
|
+
return;
|
|
482
|
+
if (now.kind === 'file' && now.revision === after) {
|
|
483
|
+
const mediaType = journal.version(historyScope, path, after)?.mediaType ?? mediaTypeOf(path);
|
|
484
|
+
const before = write.before ?? undefined;
|
|
485
|
+
// The replaced bytes are kept only when they really are that revision.
|
|
486
|
+
if (before && before.revision !== after && before.bytes instanceof Uint8Array && await blobRevision(before.bytes) === identifier(before.revision)) {
|
|
487
|
+
journal.remember(historyScope, path, { revision: before.revision, bytes: Uint8Array.from(before.bytes), mediaType: journal.version(historyScope, path, before.revision)?.mediaType ?? mediaType, savedAt: 0, source: 'observed' });
|
|
488
|
+
}
|
|
489
|
+
journal.remember(historyScope, path, { revision: after, bytes: now.bytes, mediaType, savedAt: Date.now(), source: 'agent' });
|
|
490
|
+
}
|
|
491
|
+
emit({ path, revision: now.kind === 'file' ? now.revision : null, source: 'agent' }, conversationId === undefined ? {} : { conversationId });
|
|
492
|
+
};
|
|
493
|
+
// ---- The watcher: open while someone listens to the feed (and `lingerMs` after), when the file system can watch ----------------
|
|
494
|
+
let listening = 0;
|
|
495
|
+
let watcher;
|
|
496
|
+
let watchUnsupported = false, watchedBefore = false;
|
|
497
|
+
let linger;
|
|
498
|
+
let watchWork = Promise.resolve();
|
|
499
|
+
const relativeTo = (absolutePath, base) => {
|
|
500
|
+
base = base.replace(/\/+$/, '') || '/';
|
|
501
|
+
if (absolutePath === base)
|
|
502
|
+
return '';
|
|
503
|
+
const prefix = base === '/' ? '/' : `${base}/`;
|
|
504
|
+
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : undefined;
|
|
505
|
+
};
|
|
506
|
+
async function watched(input) {
|
|
507
|
+
// The watcher may name the root as given or as resolved (a symbolic link on the way to the workspace).
|
|
508
|
+
const real = await realRoot();
|
|
509
|
+
const path = relativeTo(input, root) ?? (real === undefined ? undefined : relativeTo(input, real));
|
|
510
|
+
if (path === undefined || isTemporary(path) || path.split('/').some(name => name === '.git' || name === 'node_modules'))
|
|
511
|
+
return;
|
|
512
|
+
if (path === '') {
|
|
513
|
+
emit({ path, revision: null, source: 'watch' }, { directory: true });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const info = await fs.fileInfo(absolute(path), fsContext());
|
|
517
|
+
if (info.ok && info.value.kind === 'directory') {
|
|
518
|
+
emit({ path, revision: null, source: 'watch' }, { directory: true });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const now = await observe(path);
|
|
522
|
+
if (now.kind === 'error' || now.kind === 'outside')
|
|
523
|
+
return;
|
|
524
|
+
const revision = now.kind === 'file' ? now.revision : null;
|
|
525
|
+
if (known.has(path) && known.get(path) === revision)
|
|
526
|
+
return;
|
|
527
|
+
emit({ path, revision, source: 'watch' });
|
|
528
|
+
}
|
|
529
|
+
function onWatch(change) {
|
|
530
|
+
if ('paths' in change) {
|
|
531
|
+
const paths = [...change.paths];
|
|
532
|
+
watchWork = watchWork.then(async () => { for (const path of paths)
|
|
533
|
+
await watched(path).catch(() => undefined); });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// Overflow or a failed watcher: what changed is unknown, so every listener reloads. A failed watcher is not reopened (it would
|
|
537
|
+
// fail again, for example on a tree past the environment's limit); the host's `poll()` at turn end remains.
|
|
538
|
+
if ('error' in change) {
|
|
539
|
+
watcher = undefined;
|
|
540
|
+
watchUnsupported = true;
|
|
541
|
+
}
|
|
542
|
+
append({ kind: 'resnapshot' });
|
|
543
|
+
}
|
|
544
|
+
function startWatching() {
|
|
545
|
+
if (lifetime.signal.aborted)
|
|
546
|
+
return;
|
|
547
|
+
if (linger !== undefined) {
|
|
548
|
+
clearTimeout(linger);
|
|
549
|
+
linger = undefined;
|
|
550
|
+
}
|
|
551
|
+
if (watcher || watchUnsupported)
|
|
552
|
+
return;
|
|
553
|
+
// Changes made while nobody watched were not seen: a listener resuming from before that reloads.
|
|
554
|
+
if (watchedBefore)
|
|
555
|
+
append({ kind: 'resnapshot' });
|
|
556
|
+
watchedBefore = true;
|
|
557
|
+
watcher = fs.watch([{ path: root, recursive: true, exclude: { names: ['.git', 'node_modules'] } }], onWatch, fsContext()).then(result => {
|
|
558
|
+
if (result.ok)
|
|
559
|
+
return result.value;
|
|
560
|
+
watchUnsupported = true;
|
|
561
|
+
return undefined;
|
|
562
|
+
}, () => { watchUnsupported = true; return undefined; });
|
|
563
|
+
}
|
|
564
|
+
function stopWatching() {
|
|
565
|
+
const current = watcher;
|
|
566
|
+
watcher = undefined;
|
|
567
|
+
void current?.then(value => value?.close(fsContext()));
|
|
568
|
+
}
|
|
569
|
+
async function* changes(request, context) {
|
|
570
|
+
const access = accessSnapshot(context);
|
|
571
|
+
identifier(access.scopeId);
|
|
572
|
+
const since = request.since;
|
|
573
|
+
if (since !== undefined && (!Number.isSafeInteger(since) || since < 0))
|
|
574
|
+
throw new TypeError('Invalid change cursor');
|
|
575
|
+
if (lifetime.signal.aborted)
|
|
576
|
+
return;
|
|
577
|
+
const signals = [request.signal, access.signal, lifetime.signal].filter((value) => value !== undefined);
|
|
578
|
+
const signal = signals.length ? AbortSignal.any(signals) : new AbortController().signal;
|
|
579
|
+
listening += 1;
|
|
580
|
+
startWatching();
|
|
581
|
+
try {
|
|
582
|
+
let cursor = head;
|
|
583
|
+
if (since !== undefined) {
|
|
584
|
+
const oldest = buffer[0]?.seq ?? head + 1;
|
|
585
|
+
const foreign = request.incarnation !== undefined && request.incarnation !== incarnationOfFeed;
|
|
586
|
+
if (foreign || since > head || since < firstSeq || since < oldest - 1)
|
|
587
|
+
yield { kind: 'resnapshot', seq: head, at: Date.now() };
|
|
588
|
+
else
|
|
589
|
+
cursor = since;
|
|
590
|
+
}
|
|
591
|
+
while (!signal.aborted) {
|
|
592
|
+
const oldest = buffer[0];
|
|
593
|
+
// This listener fell behind the retained window: it reloads, then continues with what is retained.
|
|
594
|
+
if (oldest && oldest.seq > cursor + 1) {
|
|
595
|
+
cursor = oldest.seq - 1;
|
|
596
|
+
yield { kind: 'resnapshot', seq: cursor, at: Date.now() };
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const next = buffer.find(event => event.seq > cursor);
|
|
600
|
+
if (next) {
|
|
601
|
+
cursor = next.seq;
|
|
602
|
+
// The access projection: nothing after revocation, never the provider's temporary files.
|
|
603
|
+
if (signal.aborted)
|
|
604
|
+
return;
|
|
605
|
+
if (next.kind === 'change' && isTemporary(next.path))
|
|
606
|
+
continue;
|
|
607
|
+
yield next;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
await new Promise(resolve => {
|
|
611
|
+
const wake = () => { waiting.delete(wake); signal.removeEventListener('abort', wake); resolve(); };
|
|
612
|
+
waiting.add(wake);
|
|
613
|
+
signal.addEventListener('abort', wake, { once: true });
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
listening -= 1;
|
|
619
|
+
if (listening === 0 && watcher && !lifetime.signal.aborted) {
|
|
620
|
+
linger = setTimeout(() => { linger = undefined; if (listening === 0)
|
|
621
|
+
stopWatching(); }, lingerMs);
|
|
622
|
+
linger.unref?.();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
394
626
|
return {
|
|
395
627
|
providerId, read, queue, keep, identity: Object.freeze(key),
|
|
396
628
|
catalog: createWorkspaceCatalog({ fs, ...(options.catalog ? { config: options.catalog } : {}), saves: path => journal.saves(historyScope, path) }),
|
|
397
|
-
publication: { publish }, reconciliation: { lookup }, poll,
|
|
629
|
+
publication: { publish }, reconciliation: { lookup }, poll, record, changes, changeHead: () => head, changeIncarnation: incarnationOfFeed,
|
|
630
|
+
lifetime: lifetime.signal,
|
|
631
|
+
close: () => {
|
|
632
|
+
// Every change stream ends, the watcher closes and the listeners are dropped: nothing of this provider keeps running.
|
|
633
|
+
lifetime.abort();
|
|
634
|
+
if (linger !== undefined) {
|
|
635
|
+
clearTimeout(linger);
|
|
636
|
+
linger = undefined;
|
|
637
|
+
}
|
|
638
|
+
stopWatching();
|
|
639
|
+
listeners.clear();
|
|
640
|
+
for (const wake of [...waiting])
|
|
641
|
+
wake();
|
|
642
|
+
},
|
|
398
643
|
history: path => journal.revisions(historyScope, locator({ resource: { providerId, path }, view: { kind: 'published' } }).resource.path),
|
|
399
644
|
saves: path => journal.saves(historyScope, locator({ resource: { providerId, path }, view: { kind: 'published' } }).resource.path),
|
|
400
645
|
onChange: listener => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-files",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -81,12 +81,16 @@
|
|
|
81
81
|
"./revision-handler": {
|
|
82
82
|
"types": "./dist/revision-handler.d.ts",
|
|
83
83
|
"import": "./dist/revision-handler.js"
|
|
84
|
+
},
|
|
85
|
+
"./file-types": {
|
|
86
|
+
"types": "./dist/file-types.d.ts",
|
|
87
|
+
"import": "./dist/file-types.js"
|
|
84
88
|
}
|
|
85
89
|
},
|
|
86
90
|
"peerDependencies": {
|
|
87
|
-
"isomorphic-git": "1.42.6",
|
|
88
|
-
"@earendil-works/pi-durable": "1.1.0",
|
|
89
|
-
"@earendil-works/chord": "1.1.0"
|
|
91
|
+
"isomorphic-git": "^1.42.6",
|
|
92
|
+
"@earendil-works/pi-durable": "^1.1.0",
|
|
93
|
+
"@earendil-works/chord": "^1.1.0"
|
|
90
94
|
},
|
|
91
95
|
"peerDependenciesMeta": {
|
|
92
96
|
"isomorphic-git": {
|