@loomwork/plugin-content 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +92 -2
- package/dist/CachingContentStore.d.ts +54 -0
- package/dist/CachingContentStore.d.ts.map +1 -0
- package/dist/CachingContentStore.js +171 -0
- package/dist/CachingContentStore.js.map +1 -0
- package/dist/ContentResolvingEventStore.d.ts +70 -0
- package/dist/ContentResolvingEventStore.d.ts.map +1 -0
- package/dist/ContentResolvingEventStore.js +245 -0
- package/dist/ContentResolvingEventStore.js.map +1 -0
- package/dist/ContentStore.d.ts +186 -0
- package/dist/ContentStore.d.ts.map +1 -0
- package/dist/ContentStore.js +177 -0
- package/dist/ContentStore.js.map +1 -0
- package/dist/ContentTouchingSnapshotStore.d.ts +56 -0
- package/dist/ContentTouchingSnapshotStore.d.ts.map +1 -0
- package/dist/ContentTouchingSnapshotStore.js +85 -0
- package/dist/ContentTouchingSnapshotStore.js.map +1 -0
- package/dist/citations.d.ts +40 -0
- package/dist/citations.d.ts.map +1 -0
- package/dist/citations.js +118 -0
- package/dist/citations.js.map +1 -0
- package/dist/content.bench.d.ts +2 -0
- package/dist/content.bench.d.ts.map +1 -0
- package/dist/content.bench.js +75 -0
- package/dist/content.bench.js.map +1 -0
- package/dist/content.d.ts +40 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +177 -0
- package/dist/content.js.map +1 -0
- package/dist/derive.d.ts +9 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +40 -0
- package/dist/derive.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -0
- package/dist/executionEnd.d.ts +22 -0
- package/dist/executionEnd.d.ts.map +1 -0
- package/dist/executionEnd.js +66 -0
- package/dist/executionEnd.js.map +1 -0
- package/dist/gc.d.ts +470 -0
- package/dist/gc.d.ts.map +1 -0
- package/dist/gc.js +819 -0
- package/dist/gc.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/touchAround.d.ts +46 -0
- package/dist/touchAround.d.ts.map +1 -0
- package/dist/touchAround.js +98 -0
- package/dist/touchAround.js.map +1 -0
- package/package.json +58 -3
- package/src/CachingContentStore.ts +184 -0
- package/src/ContentResolvingEventStore.ts +325 -0
- package/src/ContentStore.ts +317 -0
- package/src/ContentTouchingSnapshotStore.ts +109 -0
- package/src/citations.ts +117 -0
- package/src/content.bench.ts +84 -0
- package/src/content.ts +191 -0
- package/src/derive.ts +40 -0
- package/src/errors.ts +19 -0
- package/src/executionEnd.ts +64 -0
- package/src/gc.ts +1213 -0
- package/src/index.ts +54 -0
- package/src/touchAround.ts +131 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type ContentRef,
|
|
3
|
+
collectContentRefs,
|
|
4
|
+
contentAddress,
|
|
5
|
+
decodeValue,
|
|
6
|
+
encodeValue,
|
|
7
|
+
hasContentRef,
|
|
8
|
+
isContentRef,
|
|
9
|
+
substituteContentRefs,
|
|
10
|
+
} from "./content";
|
|
11
|
+
export {
|
|
12
|
+
CONTENT_LIST_PAGE_DEFAULT_LIMIT,
|
|
13
|
+
CONTENT_LIST_PAGE_MAX_LIMIT,
|
|
14
|
+
CONTENT_STORE_SPI_VERSION,
|
|
15
|
+
type ContentAccess,
|
|
16
|
+
type ContentListPage,
|
|
17
|
+
type ContentListPageOptions,
|
|
18
|
+
type ContentPageEntry,
|
|
19
|
+
type ContentStore,
|
|
20
|
+
contentListPageLimit,
|
|
21
|
+
InMemoryContentStore,
|
|
22
|
+
type InMemoryContentStoreOptions,
|
|
23
|
+
} from "./ContentStore";
|
|
24
|
+
export { CachingContentStore, type CachingContentStoreOptions } from "./CachingContentStore";
|
|
25
|
+
export {
|
|
26
|
+
ContentResolvingEventStore,
|
|
27
|
+
type ContentResolvingEventStoreOptions,
|
|
28
|
+
} from "./ContentResolvingEventStore";
|
|
29
|
+
export {
|
|
30
|
+
CONTENT_TOUCHING_SNAPSHOT_STORE,
|
|
31
|
+
ContentTouchingSnapshotStore,
|
|
32
|
+
type ContentTouchingSnapshotStoreOptions,
|
|
33
|
+
isContentTouchingSnapshotStore,
|
|
34
|
+
} from "./ContentTouchingSnapshotStore";
|
|
35
|
+
export type { TouchErrorHook, TouchReport } from "./touchAround";
|
|
36
|
+
export { ContentMissingError } from "./errors";
|
|
37
|
+
export { externalizePathsFromSteps } from "./derive";
|
|
38
|
+
export {
|
|
39
|
+
ReclaimOrphansAckError,
|
|
40
|
+
MIN_ORPHAN_ERASE_MARGIN_MS,
|
|
41
|
+
ReclaimOrphansClockError,
|
|
42
|
+
ReclaimOrphansMarginError,
|
|
43
|
+
ReclaimOrphansSnapshotAckError,
|
|
44
|
+
ReclaimOrphansTouchError,
|
|
45
|
+
ArchiveStoresRequiredError,
|
|
46
|
+
SealedArchiveScanError,
|
|
47
|
+
SealedSnapshotScanError,
|
|
48
|
+
pinByEventKinds,
|
|
49
|
+
sweepUnpinnedContent,
|
|
50
|
+
type ContentSweepOptions,
|
|
51
|
+
type ContentSweepReport,
|
|
52
|
+
type ReclaimOrphansAck,
|
|
53
|
+
type SnapshotLookup,
|
|
54
|
+
} from "./gc";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ContentStore } from "./ContentStore";
|
|
2
|
+
import { visitCitedAddresses } from "./citations";
|
|
3
|
+
import { ContentMissingError } from "./errors";
|
|
4
|
+
|
|
5
|
+
/** What a {@link TouchErrorHook} is told about a touch that did not do its job. */
|
|
6
|
+
export interface TouchReport {
|
|
7
|
+
/** Which touch: the one BEFORE the write (a failure there aborts it) or the one AFTER it. */
|
|
8
|
+
phase: "before" | "after";
|
|
9
|
+
/** The write the touch guards. */
|
|
10
|
+
operation: "append" | "appendBatch" | "appendIfUnchanged" | "copyPrefix" | "snapshotSave";
|
|
11
|
+
executionId?: string;
|
|
12
|
+
/** Every address the write cites. */
|
|
13
|
+
addresses: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* The BARE refs among {@link addresses} that the store reported missing (non-empty only for a
|
|
16
|
+
* missing-blob report): the write cites a blob that is already gone, so the data loss has
|
|
17
|
+
* already happened and replay of that execution will throw `ContentMissingError`. A claim-check
|
|
18
|
+
* envelope's own `ref` is excluded: it may be reclaimed on a TTL by design (ADR 0055 D4).
|
|
19
|
+
*/
|
|
20
|
+
missing: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Observer for touches that failed or found a cited blob gone. `err` is whatever `touch()`
|
|
25
|
+
* threw (the AFTER touch, which is best-effort), or a {@link ContentMissingError} naming the
|
|
26
|
+
* first missing address. It is called synchronously, must not throw (a throw is swallowed, so a
|
|
27
|
+
* broken logger cannot fail a durable write), and is never called when the touch succeeded and
|
|
28
|
+
* every cited blob was present.
|
|
29
|
+
*/
|
|
30
|
+
export type TouchErrorHook = (err: unknown, info: TouchReport) => void;
|
|
31
|
+
|
|
32
|
+
let warnedTouch = false;
|
|
33
|
+
/**
|
|
34
|
+
* No `onTouchError` hook: a failed after-touch or a missing bare ref would be invisible, so say it
|
|
35
|
+
* ONCE per process (loom's other advisories are per process too, e.g. `LOOM_AGENT_RING`). Per
|
|
36
|
+
* process, not per instance: a failing content store fails on every write, and one warning per
|
|
37
|
+
* write or per store instance would flood stderr; the hook is the way to see each occurrence.
|
|
38
|
+
*/
|
|
39
|
+
function warnTouchOnce(err: unknown, info: TouchReport): void {
|
|
40
|
+
if (warnedTouch) return;
|
|
41
|
+
warnedTouch = true;
|
|
42
|
+
const proc = (globalThis as { process?: { emitWarning?: (m: string, o?: object) => void } })
|
|
43
|
+
.process;
|
|
44
|
+
const what =
|
|
45
|
+
info.missing.length > 0
|
|
46
|
+
? `a cited blob is already gone (${info.missing[0]}); replay of ${info.executionId ?? "that execution"} will throw ContentMissingError`
|
|
47
|
+
: `the ${info.phase}-write touch failed (${err instanceof Error ? err.message : String(err)})`;
|
|
48
|
+
proc?.emitWarning?.(
|
|
49
|
+
`ContentResolvingEventStore/ContentTouchingSnapshotStore (${info.operation}): ${what}. Further touch problems in this process are not repeated; pass onTouchError to observe each one.`,
|
|
50
|
+
{ code: "LOOM_CONTENT_TOUCH" }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function report(hook: TouchErrorHook | undefined, err: unknown, info: TouchReport): void {
|
|
55
|
+
try {
|
|
56
|
+
if (hook === undefined) {
|
|
57
|
+
warnTouchOnce(err, info);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const result: unknown = hook(err, info);
|
|
61
|
+
// An async hook's rejection is not ours to surface, and must not become an unhandled one.
|
|
62
|
+
if (
|
|
63
|
+
typeof result === "object" &&
|
|
64
|
+
result !== null &&
|
|
65
|
+
typeof (result as { then?: unknown }).then === "function"
|
|
66
|
+
) {
|
|
67
|
+
(result as PromiseLike<unknown>).then(undefined, () => {});
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
/* an observer must never change a write's outcome */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Touch every blob the `payloads` cite around `write` (ADR 0055 D6 amendment 2): BEFORE the
|
|
76
|
+
* write (an error aborts it; nothing landed), and AFTER it (best-effort: the write is durable, so
|
|
77
|
+
* its outcome must not turn into a failure, but an error is reported through `hook`).
|
|
78
|
+
*
|
|
79
|
+
* The guarantee is D5's, not this function's: the cutoff of a sweep is
|
|
80
|
+
* `touchClock() - olderThanMs`, so the BEFORE touch protects a write whose touch-to-commit window
|
|
81
|
+
* is shorter than that margin (an erasing orphan pass enforces a minimum margin). The AFTER touch
|
|
82
|
+
* only extends that window for a write slower than the margin; it is not what closes the race.
|
|
83
|
+
*
|
|
84
|
+
* A non-empty `missing` result from either touch is reported through the hook, never thrown: a
|
|
85
|
+
* cited blob that is gone cannot be restored by failing this write, and a fork of a log with
|
|
86
|
+
* a reclaimed claim-check envelope is legitimate. Nothing is touched, and no walk beyond the
|
|
87
|
+
* payloads' own, when the store has no `touch` or the payloads cite nothing.
|
|
88
|
+
*/
|
|
89
|
+
export async function touchAround<T>(
|
|
90
|
+
content: Pick<ContentStore, "touch">,
|
|
91
|
+
payloads: readonly unknown[],
|
|
92
|
+
ctx: { operation: TouchReport["operation"]; executionId?: string | undefined },
|
|
93
|
+
hook: TouchErrorHook | undefined,
|
|
94
|
+
write: () => Promise<T>
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
const touch = content.touch;
|
|
97
|
+
if (touch === undefined) return write();
|
|
98
|
+
const cited = new Set<string>();
|
|
99
|
+
const bare = new Set<string>();
|
|
100
|
+
for (const payload of payloads) {
|
|
101
|
+
if (typeof payload === "object" && payload !== null) {
|
|
102
|
+
visitCitedAddresses(payload, (address, inEnvelope) => {
|
|
103
|
+
cited.add(address);
|
|
104
|
+
if (!inEnvelope) bare.add(address);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (cited.size === 0) return write();
|
|
109
|
+
const addresses = [...cited];
|
|
110
|
+
const info = (phase: TouchReport["phase"], missing: readonly string[]): TouchReport => ({
|
|
111
|
+
phase,
|
|
112
|
+
operation: ctx.operation,
|
|
113
|
+
...(ctx.executionId !== undefined && { executionId: ctx.executionId }),
|
|
114
|
+
addresses,
|
|
115
|
+
missing,
|
|
116
|
+
});
|
|
117
|
+
const reportMissing = (phase: TouchReport["phase"], missing: readonly string[]): void => {
|
|
118
|
+
const lost = missing.filter((a) => bare.has(a));
|
|
119
|
+
if (lost.length > 0) {
|
|
120
|
+
report(hook, new ContentMissingError(lost[0] as string, ctx.executionId), info(phase, lost));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
reportMissing("before", await touch.call(content, addresses));
|
|
124
|
+
const result = await write();
|
|
125
|
+
try {
|
|
126
|
+
reportMissing("after", await touch.call(content, addresses));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
report(hook, err, info("after", []));
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|