@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.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +92 -2
  4. package/dist/CachingContentStore.d.ts +54 -0
  5. package/dist/CachingContentStore.d.ts.map +1 -0
  6. package/dist/CachingContentStore.js +171 -0
  7. package/dist/CachingContentStore.js.map +1 -0
  8. package/dist/ContentResolvingEventStore.d.ts +70 -0
  9. package/dist/ContentResolvingEventStore.d.ts.map +1 -0
  10. package/dist/ContentResolvingEventStore.js +245 -0
  11. package/dist/ContentResolvingEventStore.js.map +1 -0
  12. package/dist/ContentStore.d.ts +186 -0
  13. package/dist/ContentStore.d.ts.map +1 -0
  14. package/dist/ContentStore.js +177 -0
  15. package/dist/ContentStore.js.map +1 -0
  16. package/dist/ContentTouchingSnapshotStore.d.ts +56 -0
  17. package/dist/ContentTouchingSnapshotStore.d.ts.map +1 -0
  18. package/dist/ContentTouchingSnapshotStore.js +85 -0
  19. package/dist/ContentTouchingSnapshotStore.js.map +1 -0
  20. package/dist/citations.d.ts +40 -0
  21. package/dist/citations.d.ts.map +1 -0
  22. package/dist/citations.js +118 -0
  23. package/dist/citations.js.map +1 -0
  24. package/dist/content.bench.d.ts +2 -0
  25. package/dist/content.bench.d.ts.map +1 -0
  26. package/dist/content.bench.js +75 -0
  27. package/dist/content.bench.js.map +1 -0
  28. package/dist/content.d.ts +40 -0
  29. package/dist/content.d.ts.map +1 -0
  30. package/dist/content.js +177 -0
  31. package/dist/content.js.map +1 -0
  32. package/dist/derive.d.ts +9 -0
  33. package/dist/derive.d.ts.map +1 -0
  34. package/dist/derive.js +40 -0
  35. package/dist/derive.js.map +1 -0
  36. package/dist/errors.d.ts +6 -0
  37. package/dist/errors.d.ts.map +1 -0
  38. package/dist/errors.js +20 -0
  39. package/dist/errors.js.map +1 -0
  40. package/dist/executionEnd.d.ts +22 -0
  41. package/dist/executionEnd.d.ts.map +1 -0
  42. package/dist/executionEnd.js +66 -0
  43. package/dist/executionEnd.js.map +1 -0
  44. package/dist/gc.d.ts +470 -0
  45. package/dist/gc.d.ts.map +1 -0
  46. package/dist/gc.js +819 -0
  47. package/dist/gc.js.map +1 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +42 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/touchAround.d.ts +46 -0
  53. package/dist/touchAround.d.ts.map +1 -0
  54. package/dist/touchAround.js +98 -0
  55. package/dist/touchAround.js.map +1 -0
  56. package/package.json +58 -3
  57. package/src/CachingContentStore.ts +184 -0
  58. package/src/ContentResolvingEventStore.ts +325 -0
  59. package/src/ContentStore.ts +317 -0
  60. package/src/ContentTouchingSnapshotStore.ts +109 -0
  61. package/src/citations.ts +117 -0
  62. package/src/content.bench.ts +84 -0
  63. package/src/content.ts +191 -0
  64. package/src/derive.ts +40 -0
  65. package/src/errors.ts +19 -0
  66. package/src/executionEnd.ts +64 -0
  67. package/src/gc.ts +1213 -0
  68. package/src/index.ts +54 -0
  69. 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
+ }