wire-mesh-core 1.30.0 → 1.30.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.
@@ -0,0 +1,81 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_fs_promises = require("node:fs/promises");
25
+ node_fs_promises = __toESM(node_fs_promises, 1);
26
+ let node_path = require("node:path");
27
+ node_path = __toESM(node_path, 1);
28
+ //#region src/adapters/node-fs-storage.ts
29
+ /** A Node filesystem-backed KeyValueStorage -- the durable Node-side counterpart to indexeddb-storage.ts's browser one, and memory-storage.ts's in-process one, satisfying the identical contract. Every key is treated directly as a relative path under the given directory (creating nested directories as needed), which every key this codebase constructs is already suited to: internally generated, ASCII, path-shaped by design (e.g. "data/<peer-hex>/entry/<seq>") rather than arbitrary untrusted input, matching the same assumption indexeddb-storage.ts's own comment already states for its key range bounds. */
30
+ function isEnoent(error) {
31
+ if (!(error instanceof Error)) return false;
32
+ if (!("code" in error)) return false;
33
+ return error.code === "ENOENT";
34
+ }
35
+ function keyToFilePath(baseDir, key) {
36
+ return node_path.join(baseDir, key);
37
+ }
38
+ /** Every regular file under baseDir, as a relative path using "/" separators regardless of the host OS -- so a caller's own "/"-joined key strings compare correctly against what this returns, the same key shape set()/get() already use. */
39
+ async function listAllRelativePaths(baseDir) {
40
+ let entries;
41
+ try {
42
+ entries = await node_fs_promises.readdir(baseDir, { recursive: true });
43
+ } catch (error) {
44
+ if (isEnoent(error)) return [];
45
+ throw error;
46
+ }
47
+ const files = [];
48
+ for (const entry of entries) if ((await node_fs_promises.stat(node_path.join(baseDir, entry)).catch(() => null))?.isFile() === true) files.push(entry.split(node_path.sep).join("/"));
49
+ return files;
50
+ }
51
+ function createNodeFsStorage(options) {
52
+ const baseDir = options.dir;
53
+ return {
54
+ async get(key) {
55
+ try {
56
+ const contents = await node_fs_promises.readFile(keyToFilePath(baseDir, key));
57
+ return new Uint8Array(contents);
58
+ } catch (error) {
59
+ if (isEnoent(error)) return void 0;
60
+ throw error;
61
+ }
62
+ },
63
+ async set(key, value) {
64
+ const filePath = keyToFilePath(baseDir, key);
65
+ await node_fs_promises.mkdir(node_path.dirname(filePath), { recursive: true });
66
+ await node_fs_promises.writeFile(filePath, value);
67
+ },
68
+ async delete(key) {
69
+ try {
70
+ await node_fs_promises.unlink(keyToFilePath(baseDir, key));
71
+ } catch (error) {
72
+ if (!isEnoent(error)) throw error;
73
+ }
74
+ },
75
+ async keys(prefix) {
76
+ return (await listAllRelativePaths(baseDir)).filter((key) => key.startsWith(prefix));
77
+ }
78
+ };
79
+ }
80
+ //#endregion
81
+ exports.createNodeFsStorage = createNodeFsStorage;
@@ -0,0 +1,8 @@
1
+ import { KeyValueStorage } from "../ports/storage.cjs";
2
+ //#region src/adapters/node-fs-storage.d.ts
3
+ export interface NodeFsStorageOptions {
4
+ /** Directory to store entries under. Created on first write if it doesn't already exist. */
5
+ dir: string;
6
+ }
7
+ export declare function createNodeFsStorage(options: Readonly<NodeFsStorageOptions>): KeyValueStorage;
8
+ //#endregion
@@ -0,0 +1,8 @@
1
+ import { KeyValueStorage } from "../ports/storage.mjs";
2
+ //#region src/adapters/node-fs-storage.d.ts
3
+ export interface NodeFsStorageOptions {
4
+ /** Directory to store entries under. Created on first write if it doesn't already exist. */
5
+ dir: string;
6
+ }
7
+ export declare function createNodeFsStorage(options: Readonly<NodeFsStorageOptions>): KeyValueStorage;
8
+ //#endregion
@@ -0,0 +1,56 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ //#region src/adapters/node-fs-storage.ts
4
+ /** A Node filesystem-backed KeyValueStorage -- the durable Node-side counterpart to indexeddb-storage.ts's browser one, and memory-storage.ts's in-process one, satisfying the identical contract. Every key is treated directly as a relative path under the given directory (creating nested directories as needed), which every key this codebase constructs is already suited to: internally generated, ASCII, path-shaped by design (e.g. "data/<peer-hex>/entry/<seq>") rather than arbitrary untrusted input, matching the same assumption indexeddb-storage.ts's own comment already states for its key range bounds. */
5
+ function isEnoent(error) {
6
+ if (!(error instanceof Error)) return false;
7
+ if (!("code" in error)) return false;
8
+ return error.code === "ENOENT";
9
+ }
10
+ function keyToFilePath(baseDir, key) {
11
+ return path.join(baseDir, key);
12
+ }
13
+ /** Every regular file under baseDir, as a relative path using "/" separators regardless of the host OS -- so a caller's own "/"-joined key strings compare correctly against what this returns, the same key shape set()/get() already use. */
14
+ async function listAllRelativePaths(baseDir) {
15
+ let entries;
16
+ try {
17
+ entries = await fs.readdir(baseDir, { recursive: true });
18
+ } catch (error) {
19
+ if (isEnoent(error)) return [];
20
+ throw error;
21
+ }
22
+ const files = [];
23
+ for (const entry of entries) if ((await fs.stat(path.join(baseDir, entry)).catch(() => null))?.isFile() === true) files.push(entry.split(path.sep).join("/"));
24
+ return files;
25
+ }
26
+ function createNodeFsStorage(options) {
27
+ const baseDir = options.dir;
28
+ return {
29
+ async get(key) {
30
+ try {
31
+ const contents = await fs.readFile(keyToFilePath(baseDir, key));
32
+ return new Uint8Array(contents);
33
+ } catch (error) {
34
+ if (isEnoent(error)) return void 0;
35
+ throw error;
36
+ }
37
+ },
38
+ async set(key, value) {
39
+ const filePath = keyToFilePath(baseDir, key);
40
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
41
+ await fs.writeFile(filePath, value);
42
+ },
43
+ async delete(key) {
44
+ try {
45
+ await fs.unlink(keyToFilePath(baseDir, key));
46
+ } catch (error) {
47
+ if (!isEnoent(error)) throw error;
48
+ }
49
+ },
50
+ async keys(prefix) {
51
+ return (await listAllRelativePaths(baseDir)).filter((key) => key.startsWith(prefix));
52
+ }
53
+ };
54
+ }
55
+ //#endregion
56
+ export { createNodeFsStorage };
@@ -0,0 +1,109 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_domain_device_id = require("./device-id.cjs");
3
+ //#region src/domain/data-sync.ts
4
+ function headKey(peer) {
5
+ return `data/${require_domain_device_id.deviceIdToHex(peer)}/head`;
6
+ }
7
+ function entryKey(peer, seq) {
8
+ return `data/${require_domain_device_id.deviceIdToHex(peer)}/entry/${String(seq)}`;
9
+ }
10
+ async function readHeadSeq(storage, peer) {
11
+ const raw = await storage.get(headKey(peer));
12
+ if (raw === void 0) return 0;
13
+ return Number(new TextDecoder().decode(raw));
14
+ }
15
+ async function writeHeadSeq(storage, peer, seq) {
16
+ await storage.set(headKey(peer), new TextEncoder().encode(String(seq)));
17
+ }
18
+ /**
19
+ * The current head-seq this store holds for `peer`'s log -- own log if peer is this device's own identity, a replicated peer's log otherwise (any store may hold and answer for a peer's log, per data-domain.cddl's own fan-out rule; nothing here distinguishes "my own log" from "a replica I'm holding"). 0 means nothing is held yet.
20
+ */
21
+ async function headSeqFor(storage, peer) {
22
+ return readHeadSeq(storage, peer);
23
+ }
24
+ /**
25
+ * Appends one entry to this identity's own oplog and returns both the new sequence and the data-have frame announcing it. Only manages the local append and head-seq bookkeeping -- the caller decides when and to which peers to actually send the returned frame (proactively on every append, batched, on a timer, or not at all), the same "primitive vs. policy" split sendDirectManageRequest already draws for the manage-request path.
26
+ */
27
+ async function appendOwnEntry(options, entry) {
28
+ const seq = await readHeadSeq(options.storage, options.identity.deviceId) + 1;
29
+ await options.storage.set(entryKey(options.identity.deviceId, seq), entry);
30
+ await writeHeadSeq(options.storage, options.identity.deviceId, seq);
31
+ return {
32
+ seq,
33
+ haveFrame: {
34
+ type: "data-have",
35
+ peer: options.identity.deviceId,
36
+ "head-seq": seq
37
+ }
38
+ };
39
+ }
40
+ /**
41
+ * Handles an incoming data-have: null if this store already holds `frame.peer`'s log up to or past the announced head-seq (nothing to do), otherwise the data-request that would catch it up from its own current head. Whether to actually send that request -- whether this store tracks `frame.peer`'s log at all -- is the caller's own policy; this function only ever answers "would catching up mean anything right now", never "do I want to".
42
+ */
43
+ async function handleDataHave(storage, frame) {
44
+ const ourHead = await readHeadSeq(storage, frame.peer);
45
+ if (ourHead >= frame["head-seq"]) return null;
46
+ return {
47
+ type: "data-request",
48
+ peer: frame.peer,
49
+ "from-seq": ourHead
50
+ };
51
+ }
52
+ /**
53
+ * Handles an incoming data-request: the entries this store actually holds for `frame.peer` after `frame["from-seq"]`, capped at `limit`, or null if it holds none past that point. The responder need not be `frame.peer` itself -- any peer holding a replica may answer on the original author's behalf, per data-domain.cddl's own fan-out rule; this function makes no distinction.
54
+ */
55
+ async function handleDataRequest(storage, frame, limit) {
56
+ const ourHead = await readHeadSeq(storage, frame.peer);
57
+ if (ourHead <= frame["from-seq"]) return null;
58
+ const lastSeq = Math.min(ourHead, frame["from-seq"] + limit);
59
+ const entries = [];
60
+ for (let seq = frame["from-seq"] + 1; seq <= lastSeq; seq += 1) {
61
+ const entry = await storage.get(entryKey(frame.peer, seq));
62
+ if (entry === void 0) break;
63
+ entries.push(entry);
64
+ }
65
+ if (entries.length === 0) return null;
66
+ return {
67
+ type: "data-entries",
68
+ peer: frame.peer,
69
+ "from-seq": frame["from-seq"],
70
+ entries
71
+ };
72
+ }
73
+ /**
74
+ * Handles an incoming data-entries: persists the given entries starting right after from-seq, but ONLY when from-seq exactly matches this store's own current head for `frame.peer` -- data-domain.cddl's own gap-rejection obligation. A mismatch covers both directions: from-seq ahead of our head is a real gap (we're missing something in between); from-seq behind our head is a stale or replayed response we've already applied. Either way the caller's own retry path is the same -- re-issue a data-request from this store's own current headSeqFor(peer), which handleDataHave already computes correctly on the next data-have it sees.
75
+ */
76
+ async function handleDataEntries(storage, frame) {
77
+ const ourHead = await readHeadSeq(storage, frame.peer);
78
+ if (frame["from-seq"] !== ourHead) return {
79
+ ok: false,
80
+ reason: "gap"
81
+ };
82
+ let seq = frame["from-seq"];
83
+ for (const entry of frame.entries) {
84
+ seq += 1;
85
+ await storage.set(entryKey(frame.peer, seq), entry);
86
+ }
87
+ await writeHeadSeq(storage, frame.peer, seq);
88
+ return { ok: true };
89
+ }
90
+ /**
91
+ * Entries this store holds for `peer` after `fromSeqExclusive`, in sequence order -- the local read-side counterpart to appendOwnEntry/handleDataEntries, for a caller replaying a log directly (e.g. an application rendering history) rather than syncing it over the wire.
92
+ */
93
+ async function readEntries(storage, peer, fromSeqExclusive) {
94
+ const head = await readHeadSeq(storage, peer);
95
+ const entries = [];
96
+ for (let seq = fromSeqExclusive + 1; seq <= head; seq += 1) {
97
+ const entry = await storage.get(entryKey(peer, seq));
98
+ if (entry === void 0) break;
99
+ entries.push(entry);
100
+ }
101
+ return entries;
102
+ }
103
+ //#endregion
104
+ exports.appendOwnEntry = appendOwnEntry;
105
+ exports.handleDataEntries = handleDataEntries;
106
+ exports.handleDataHave = handleDataHave;
107
+ exports.handleDataRequest = handleDataRequest;
108
+ exports.headSeqFor = headSeqFor;
109
+ exports.readEntries = readEntries;
@@ -0,0 +1,42 @@
1
+ import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-sW8d6Og-.cjs";
2
+ import { KeyValueStorage } from "../ports/storage.cjs";
3
+ import { t as IdentityPort } from "../identity-DJ06jn4k.cjs";
4
+ //#region src/domain/data-sync.d.ts
5
+ export interface DataSyncOptions {
6
+ identity: IdentityPort;
7
+ storage: KeyValueStorage;
8
+ }
9
+ /**
10
+ * The current head-seq this store holds for `peer`'s log -- own log if peer is this device's own identity, a replicated peer's log otherwise (any store may hold and answer for a peer's log, per data-domain.cddl's own fan-out rule; nothing here distinguishes "my own log" from "a replica I'm holding"). 0 means nothing is held yet.
11
+ */
12
+ export declare function headSeqFor(storage: Readonly<KeyValueStorage>, peer: DeviceId): Promise<number>;
13
+ /**
14
+ * Appends one entry to this identity's own oplog and returns both the new sequence and the data-have frame announcing it. Only manages the local append and head-seq bookkeeping -- the caller decides when and to which peers to actually send the returned frame (proactively on every append, batched, on a timer, or not at all), the same "primitive vs. policy" split sendDirectManageRequest already draws for the manage-request path.
15
+ */
16
+ export declare function appendOwnEntry(options: Readonly<DataSyncOptions>, entry: Uint8Array<ArrayBuffer>): Promise<{
17
+ seq: number;
18
+ haveFrame: DataHaveFrame;
19
+ }>;
20
+ /**
21
+ * Handles an incoming data-have: null if this store already holds `frame.peer`'s log up to or past the announced head-seq (nothing to do), otherwise the data-request that would catch it up from its own current head. Whether to actually send that request -- whether this store tracks `frame.peer`'s log at all -- is the caller's own policy; this function only ever answers "would catching up mean anything right now", never "do I want to".
22
+ */
23
+ export declare function handleDataHave(storage: Readonly<KeyValueStorage>, frame: Readonly<DataHaveFrame>): Promise<DataRequestFrame | null>;
24
+ /**
25
+ * Handles an incoming data-request: the entries this store actually holds for `frame.peer` after `frame["from-seq"]`, capped at `limit`, or null if it holds none past that point. The responder need not be `frame.peer` itself -- any peer holding a replica may answer on the original author's behalf, per data-domain.cddl's own fan-out rule; this function makes no distinction.
26
+ */
27
+ export declare function handleDataRequest(storage: Readonly<KeyValueStorage>, frame: Readonly<DataRequestFrame>, limit: number): Promise<DataEntriesFrame | null>;
28
+ export type HandleDataEntriesResult = {
29
+ ok: true;
30
+ } | {
31
+ ok: false;
32
+ reason: "gap";
33
+ };
34
+ /**
35
+ * Handles an incoming data-entries: persists the given entries starting right after from-seq, but ONLY when from-seq exactly matches this store's own current head for `frame.peer` -- data-domain.cddl's own gap-rejection obligation. A mismatch covers both directions: from-seq ahead of our head is a real gap (we're missing something in between); from-seq behind our head is a stale or replayed response we've already applied. Either way the caller's own retry path is the same -- re-issue a data-request from this store's own current headSeqFor(peer), which handleDataHave already computes correctly on the next data-have it sees.
36
+ */
37
+ export declare function handleDataEntries(storage: Readonly<KeyValueStorage>, frame: Readonly<DataEntriesFrame>): Promise<HandleDataEntriesResult>;
38
+ /**
39
+ * Entries this store holds for `peer` after `fromSeqExclusive`, in sequence order -- the local read-side counterpart to appendOwnEntry/handleDataEntries, for a caller replaying a log directly (e.g. an application rendering history) rather than syncing it over the wire.
40
+ */
41
+ export declare function readEntries(storage: Readonly<KeyValueStorage>, peer: DeviceId, fromSeqExclusive: number): Promise<Uint8Array[]>;
42
+ //#endregion
@@ -0,0 +1,42 @@
1
+ import { D as DeviceId, E as DataRequestFrame, T as DataHaveFrame, w as DataEntriesFrame } from "../protocol-sW8d6Og-.mjs";
2
+ import { KeyValueStorage } from "../ports/storage.mjs";
3
+ import { t as IdentityPort } from "../identity-C5N86BPz.mjs";
4
+ //#region src/domain/data-sync.d.ts
5
+ export interface DataSyncOptions {
6
+ identity: IdentityPort;
7
+ storage: KeyValueStorage;
8
+ }
9
+ /**
10
+ * The current head-seq this store holds for `peer`'s log -- own log if peer is this device's own identity, a replicated peer's log otherwise (any store may hold and answer for a peer's log, per data-domain.cddl's own fan-out rule; nothing here distinguishes "my own log" from "a replica I'm holding"). 0 means nothing is held yet.
11
+ */
12
+ export declare function headSeqFor(storage: Readonly<KeyValueStorage>, peer: DeviceId): Promise<number>;
13
+ /**
14
+ * Appends one entry to this identity's own oplog and returns both the new sequence and the data-have frame announcing it. Only manages the local append and head-seq bookkeeping -- the caller decides when and to which peers to actually send the returned frame (proactively on every append, batched, on a timer, or not at all), the same "primitive vs. policy" split sendDirectManageRequest already draws for the manage-request path.
15
+ */
16
+ export declare function appendOwnEntry(options: Readonly<DataSyncOptions>, entry: Uint8Array<ArrayBuffer>): Promise<{
17
+ seq: number;
18
+ haveFrame: DataHaveFrame;
19
+ }>;
20
+ /**
21
+ * Handles an incoming data-have: null if this store already holds `frame.peer`'s log up to or past the announced head-seq (nothing to do), otherwise the data-request that would catch it up from its own current head. Whether to actually send that request -- whether this store tracks `frame.peer`'s log at all -- is the caller's own policy; this function only ever answers "would catching up mean anything right now", never "do I want to".
22
+ */
23
+ export declare function handleDataHave(storage: Readonly<KeyValueStorage>, frame: Readonly<DataHaveFrame>): Promise<DataRequestFrame | null>;
24
+ /**
25
+ * Handles an incoming data-request: the entries this store actually holds for `frame.peer` after `frame["from-seq"]`, capped at `limit`, or null if it holds none past that point. The responder need not be `frame.peer` itself -- any peer holding a replica may answer on the original author's behalf, per data-domain.cddl's own fan-out rule; this function makes no distinction.
26
+ */
27
+ export declare function handleDataRequest(storage: Readonly<KeyValueStorage>, frame: Readonly<DataRequestFrame>, limit: number): Promise<DataEntriesFrame | null>;
28
+ export type HandleDataEntriesResult = {
29
+ ok: true;
30
+ } | {
31
+ ok: false;
32
+ reason: "gap";
33
+ };
34
+ /**
35
+ * Handles an incoming data-entries: persists the given entries starting right after from-seq, but ONLY when from-seq exactly matches this store's own current head for `frame.peer` -- data-domain.cddl's own gap-rejection obligation. A mismatch covers both directions: from-seq ahead of our head is a real gap (we're missing something in between); from-seq behind our head is a stale or replayed response we've already applied. Either way the caller's own retry path is the same -- re-issue a data-request from this store's own current headSeqFor(peer), which handleDataHave already computes correctly on the next data-have it sees.
36
+ */
37
+ export declare function handleDataEntries(storage: Readonly<KeyValueStorage>, frame: Readonly<DataEntriesFrame>): Promise<HandleDataEntriesResult>;
38
+ /**
39
+ * Entries this store holds for `peer` after `fromSeqExclusive`, in sequence order -- the local read-side counterpart to appendOwnEntry/handleDataEntries, for a caller replaying a log directly (e.g. an application rendering history) rather than syncing it over the wire.
40
+ */
41
+ export declare function readEntries(storage: Readonly<KeyValueStorage>, peer: DeviceId, fromSeqExclusive: number): Promise<Uint8Array[]>;
42
+ //#endregion
@@ -0,0 +1,103 @@
1
+ import { deviceIdToHex } from "./device-id.mjs";
2
+ //#region src/domain/data-sync.ts
3
+ function headKey(peer) {
4
+ return `data/${deviceIdToHex(peer)}/head`;
5
+ }
6
+ function entryKey(peer, seq) {
7
+ return `data/${deviceIdToHex(peer)}/entry/${String(seq)}`;
8
+ }
9
+ async function readHeadSeq(storage, peer) {
10
+ const raw = await storage.get(headKey(peer));
11
+ if (raw === void 0) return 0;
12
+ return Number(new TextDecoder().decode(raw));
13
+ }
14
+ async function writeHeadSeq(storage, peer, seq) {
15
+ await storage.set(headKey(peer), new TextEncoder().encode(String(seq)));
16
+ }
17
+ /**
18
+ * The current head-seq this store holds for `peer`'s log -- own log if peer is this device's own identity, a replicated peer's log otherwise (any store may hold and answer for a peer's log, per data-domain.cddl's own fan-out rule; nothing here distinguishes "my own log" from "a replica I'm holding"). 0 means nothing is held yet.
19
+ */
20
+ async function headSeqFor(storage, peer) {
21
+ return readHeadSeq(storage, peer);
22
+ }
23
+ /**
24
+ * Appends one entry to this identity's own oplog and returns both the new sequence and the data-have frame announcing it. Only manages the local append and head-seq bookkeeping -- the caller decides when and to which peers to actually send the returned frame (proactively on every append, batched, on a timer, or not at all), the same "primitive vs. policy" split sendDirectManageRequest already draws for the manage-request path.
25
+ */
26
+ async function appendOwnEntry(options, entry) {
27
+ const seq = await readHeadSeq(options.storage, options.identity.deviceId) + 1;
28
+ await options.storage.set(entryKey(options.identity.deviceId, seq), entry);
29
+ await writeHeadSeq(options.storage, options.identity.deviceId, seq);
30
+ return {
31
+ seq,
32
+ haveFrame: {
33
+ type: "data-have",
34
+ peer: options.identity.deviceId,
35
+ "head-seq": seq
36
+ }
37
+ };
38
+ }
39
+ /**
40
+ * Handles an incoming data-have: null if this store already holds `frame.peer`'s log up to or past the announced head-seq (nothing to do), otherwise the data-request that would catch it up from its own current head. Whether to actually send that request -- whether this store tracks `frame.peer`'s log at all -- is the caller's own policy; this function only ever answers "would catching up mean anything right now", never "do I want to".
41
+ */
42
+ async function handleDataHave(storage, frame) {
43
+ const ourHead = await readHeadSeq(storage, frame.peer);
44
+ if (ourHead >= frame["head-seq"]) return null;
45
+ return {
46
+ type: "data-request",
47
+ peer: frame.peer,
48
+ "from-seq": ourHead
49
+ };
50
+ }
51
+ /**
52
+ * Handles an incoming data-request: the entries this store actually holds for `frame.peer` after `frame["from-seq"]`, capped at `limit`, or null if it holds none past that point. The responder need not be `frame.peer` itself -- any peer holding a replica may answer on the original author's behalf, per data-domain.cddl's own fan-out rule; this function makes no distinction.
53
+ */
54
+ async function handleDataRequest(storage, frame, limit) {
55
+ const ourHead = await readHeadSeq(storage, frame.peer);
56
+ if (ourHead <= frame["from-seq"]) return null;
57
+ const lastSeq = Math.min(ourHead, frame["from-seq"] + limit);
58
+ const entries = [];
59
+ for (let seq = frame["from-seq"] + 1; seq <= lastSeq; seq += 1) {
60
+ const entry = await storage.get(entryKey(frame.peer, seq));
61
+ if (entry === void 0) break;
62
+ entries.push(entry);
63
+ }
64
+ if (entries.length === 0) return null;
65
+ return {
66
+ type: "data-entries",
67
+ peer: frame.peer,
68
+ "from-seq": frame["from-seq"],
69
+ entries
70
+ };
71
+ }
72
+ /**
73
+ * Handles an incoming data-entries: persists the given entries starting right after from-seq, but ONLY when from-seq exactly matches this store's own current head for `frame.peer` -- data-domain.cddl's own gap-rejection obligation. A mismatch covers both directions: from-seq ahead of our head is a real gap (we're missing something in between); from-seq behind our head is a stale or replayed response we've already applied. Either way the caller's own retry path is the same -- re-issue a data-request from this store's own current headSeqFor(peer), which handleDataHave already computes correctly on the next data-have it sees.
74
+ */
75
+ async function handleDataEntries(storage, frame) {
76
+ const ourHead = await readHeadSeq(storage, frame.peer);
77
+ if (frame["from-seq"] !== ourHead) return {
78
+ ok: false,
79
+ reason: "gap"
80
+ };
81
+ let seq = frame["from-seq"];
82
+ for (const entry of frame.entries) {
83
+ seq += 1;
84
+ await storage.set(entryKey(frame.peer, seq), entry);
85
+ }
86
+ await writeHeadSeq(storage, frame.peer, seq);
87
+ return { ok: true };
88
+ }
89
+ /**
90
+ * Entries this store holds for `peer` after `fromSeqExclusive`, in sequence order -- the local read-side counterpart to appendOwnEntry/handleDataEntries, for a caller replaying a log directly (e.g. an application rendering history) rather than syncing it over the wire.
91
+ */
92
+ async function readEntries(storage, peer, fromSeqExclusive) {
93
+ const head = await readHeadSeq(storage, peer);
94
+ const entries = [];
95
+ for (let seq = fromSeqExclusive + 1; seq <= head; seq += 1) {
96
+ const entry = await storage.get(entryKey(peer, seq));
97
+ if (entry === void 0) break;
98
+ entries.push(entry);
99
+ }
100
+ return entries;
101
+ }
102
+ //#endregion
103
+ export { appendOwnEntry, handleDataEntries, handleDataHave, handleDataRequest, headSeqFor, readEntries };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "1.30.0",
3
+ "version": "1.30.1",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
6
6
  "repository": {
@@ -70,6 +70,10 @@
70
70
  "import": "./dist/adapters/memory-storage.mjs",
71
71
  "require": "./dist/adapters/memory-storage.cjs"
72
72
  },
73
+ "./adapters/node-fs-storage": {
74
+ "import": "./dist/adapters/node-fs-storage.mjs",
75
+ "require": "./dist/adapters/node-fs-storage.cjs"
76
+ },
73
77
  "./adapters/node-identity": {
74
78
  "import": "./dist/adapters/node-identity.mjs",
75
79
  "require": "./dist/adapters/node-identity.cjs"
@@ -98,6 +102,10 @@
98
102
  "import": "./dist/domain/capability-request.mjs",
99
103
  "require": "./dist/domain/capability-request.cjs"
100
104
  },
105
+ "./domain/data-sync": {
106
+ "import": "./dist/domain/data-sync.mjs",
107
+ "require": "./dist/domain/data-sync.cjs"
108
+ },
101
109
  "./domain/device-id": {
102
110
  "import": "./dist/domain/device-id.mjs",
103
111
  "require": "./dist/domain/device-id.cjs"