@loro-dev/streams-crdt 0.1.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/README.md +407 -0
- package/dist/flock.d.ts +36 -0
- package/dist/flock.js +146 -0
- package/dist/flock.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/loro.d.ts +22 -0
- package/dist/loro.js +108 -0
- package/dist/loro.js.map +1 -0
- package/dist/stream-id-1f2mhtmk.d.ts +463 -0
- package/dist/stream-id-CIhmveJD.js +2151 -0
- package/dist/stream-id-CIhmveJD.js.map +1 -0
- package/package.json +76 -0
package/dist/loro.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { a as InMemoryRemoteCursorStore, i as StreamsCrdt, n as isValidBucketId, o as IndexedDbRemoteCursorStore, r as isValidRillId, s as createInitialRemoteCursor, t as createStreamUrl } from "./stream-id-CIhmveJD.js";
|
|
2
|
+
import { VersionVector, decodeImportBlobMeta } from "loro-crdt";
|
|
3
|
+
//#region src/loro.ts
|
|
4
|
+
function toDocVersionRecord(version) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (const [peer, counter] of version.toJSON()) out[peer] = counter;
|
|
7
|
+
return out;
|
|
8
|
+
}
|
|
9
|
+
function toDocVersionMap(version) {
|
|
10
|
+
const out = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const [peer, counter] of Object.entries(version)) if (typeof counter === "number" && Number.isFinite(counter)) out.set(peer, counter);
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function mergeDocVersions(base, incoming) {
|
|
15
|
+
const merged = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const [peer, counter] of Object.entries(base)) merged.set(peer, counter);
|
|
17
|
+
for (const [peer, counter] of Object.entries(incoming)) {
|
|
18
|
+
const peerId = peer;
|
|
19
|
+
merged.set(peerId, Math.max(merged.get(peerId) ?? 0, counter));
|
|
20
|
+
}
|
|
21
|
+
return toDocVersionRecord(VersionVector.parseJSON(merged));
|
|
22
|
+
}
|
|
23
|
+
function hasUpdatesSince(fromVersion, currentVersion) {
|
|
24
|
+
for (const [peer, counter] of Object.entries(currentVersion)) if (counter > (fromVersion[peer] ?? 0)) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function extractVersionFromUpdate(update) {
|
|
28
|
+
try {
|
|
29
|
+
return toDocVersionRecord(decodeImportBlobMeta(update, false).partialEndVersionVector);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function assertDocAttached(doc) {
|
|
35
|
+
if (!doc.isDetached()) return;
|
|
36
|
+
throw new Error("LoroDoc must be attached before using streams-crdt; call checkoutToLatest() or attach() first");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates a `CrdtAdapter` for one `LoroDoc`.
|
|
40
|
+
*
|
|
41
|
+
* The `LoroDoc` must be attached (not detached or checked out to a historical
|
|
42
|
+
* version). If the document is detached, call `doc.checkoutToLatest()` or
|
|
43
|
+
* `doc.attach()` before passing it here.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const doc = new LoroDoc();
|
|
48
|
+
* const adapter = createLoroDocAdapter(doc);
|
|
49
|
+
* const transport = new StreamsCrdt({ streamUrl, adapter, auth });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
function createLoroDocAdapter(doc) {
|
|
53
|
+
return {
|
|
54
|
+
emptyVersion: () => ({}),
|
|
55
|
+
mergeVersions: (base, incoming) => mergeDocVersions(base, incoming),
|
|
56
|
+
exportSnapshot: () => {
|
|
57
|
+
assertDocAttached(doc);
|
|
58
|
+
return doc.export({ mode: "snapshot" });
|
|
59
|
+
},
|
|
60
|
+
applySnapshot: (snapshot) => {
|
|
61
|
+
assertDocAttached(doc);
|
|
62
|
+
doc.import(snapshot);
|
|
63
|
+
return toDocVersionRecord(doc.version());
|
|
64
|
+
},
|
|
65
|
+
exportUpdates: (fromVersion) => {
|
|
66
|
+
assertDocAttached(doc);
|
|
67
|
+
if (!hasUpdatesSince(fromVersion, toDocVersionRecord(doc.version()))) return;
|
|
68
|
+
const updates = [doc.export({
|
|
69
|
+
mode: "update",
|
|
70
|
+
from: VersionVector.parseJSON(toDocVersionMap(fromVersion))
|
|
71
|
+
})].filter((update) => update.byteLength > 0);
|
|
72
|
+
if (updates.length === 0) return;
|
|
73
|
+
let version = {};
|
|
74
|
+
for (const update of updates) {
|
|
75
|
+
const next = extractVersionFromUpdate(update);
|
|
76
|
+
if (next != null) version = mergeDocVersions(version, next);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
updates,
|
|
80
|
+
version
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
applyRemoteUpdates: (updates, remoterVersion) => {
|
|
84
|
+
assertDocAttached(doc);
|
|
85
|
+
if (updates.length > 0) doc.importBatch(updates);
|
|
86
|
+
let version = remoterVersion;
|
|
87
|
+
for (const update of updates) {
|
|
88
|
+
const next = extractVersionFromUpdate(update);
|
|
89
|
+
if (next != null) version = mergeDocVersions(version, next);
|
|
90
|
+
}
|
|
91
|
+
return version;
|
|
92
|
+
},
|
|
93
|
+
subscribeLocalUpdates: (listener) => {
|
|
94
|
+
assertDocAttached(doc);
|
|
95
|
+
return doc.subscribeLocalUpdates((update) => {
|
|
96
|
+
const version = extractVersionFromUpdate(update) ?? {};
|
|
97
|
+
listener({
|
|
98
|
+
updates: [update],
|
|
99
|
+
version
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, StreamsCrdt, createInitialRemoteCursor, createLoroDocAdapter, createStreamUrl, isValidBucketId, isValidRillId };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=loro.js.map
|
package/dist/loro.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loro.js","names":[],"sources":["../src/loro.ts"],"sourcesContent":["import {\n decodeImportBlobMeta,\n type LoroDoc,\n VersionVector,\n} from \"loro-crdt\";\n\nimport type { CrdtAdapter, JsonObject } from \"./types\";\n\nexport { StreamsCrdt } from \"./transport\";\nexport * from \"./shared\";\n\ntype PeerId = `${number}`;\ntype DocVersionRecord = Record<PeerId, number>;\n\nfunction toDocVersionRecord(version: VersionVector): DocVersionRecord {\n const out = {} as DocVersionRecord;\n for (const [peer, counter] of version.toJSON()) {\n out[peer as PeerId] = counter;\n }\n return out;\n}\n\nfunction toDocVersionMap(version: JsonObject): Map<PeerId, number> {\n const out = new Map<PeerId, number>();\n for (const [peer, counter] of Object.entries(version)) {\n if (typeof counter === \"number\" && Number.isFinite(counter)) {\n out.set(peer as PeerId, counter);\n }\n }\n return out;\n}\n\nfunction mergeDocVersions(\n base: DocVersionRecord,\n incoming: DocVersionRecord,\n): DocVersionRecord {\n const merged = new Map<PeerId, number>();\n for (const [peer, counter] of Object.entries(base)) {\n merged.set(peer as PeerId, counter);\n }\n for (const [peer, counter] of Object.entries(incoming)) {\n const peerId = peer as PeerId;\n merged.set(peerId, Math.max(merged.get(peerId) ?? 0, counter));\n }\n return toDocVersionRecord(VersionVector.parseJSON(merged));\n}\n\nfunction hasUpdatesSince(\n fromVersion: DocVersionRecord,\n currentVersion: DocVersionRecord,\n): boolean {\n for (const [peer, counter] of Object.entries(currentVersion)) {\n if (counter > (fromVersion[peer as PeerId] ?? 0)) {\n return true;\n }\n }\n return false;\n}\n\nfunction extractVersionFromUpdate(update: Uint8Array): DocVersionRecord | null {\n try {\n const meta = decodeImportBlobMeta(update, false);\n return toDocVersionRecord(meta.partialEndVersionVector);\n } catch {\n return null;\n }\n}\n\nfunction assertDocAttached(doc: LoroDoc): void {\n if (!doc.isDetached()) {\n return;\n }\n throw new Error(\n \"LoroDoc must be attached before using streams-crdt; \" +\n \"call checkoutToLatest() or attach() first\",\n );\n}\n\n/**\n * Creates a `CrdtAdapter` for one `LoroDoc`.\n *\n * The `LoroDoc` must be attached (not detached or checked out to a historical\n * version). If the document is detached, call `doc.checkoutToLatest()` or\n * `doc.attach()` before passing it here.\n *\n * @example\n * ```ts\n * const doc = new LoroDoc();\n * const adapter = createLoroDocAdapter(doc);\n * const transport = new StreamsCrdt({ streamUrl, adapter, auth });\n * ```\n */\nexport function createLoroDocAdapter(\n doc: LoroDoc,\n): CrdtAdapter<JsonObject> {\n return {\n emptyVersion: () => ({}),\n mergeVersions: (base, incoming) =>\n mergeDocVersions(base as DocVersionRecord, incoming as DocVersionRecord),\n exportSnapshot: () => {\n assertDocAttached(doc);\n return doc.export({ mode: \"snapshot\" });\n },\n applySnapshot: (snapshot) => {\n assertDocAttached(doc);\n doc.import(snapshot);\n return toDocVersionRecord(doc.version());\n },\n exportUpdates: (fromVersion) => {\n assertDocAttached(doc);\n const currentVersion = toDocVersionRecord(doc.version());\n if (\n !hasUpdatesSince(\n fromVersion as DocVersionRecord,\n currentVersion,\n )\n ) {\n return undefined;\n }\n const updates = [\n doc.export({\n mode: \"update\",\n from: VersionVector.parseJSON(toDocVersionMap(fromVersion)),\n }),\n ].filter((update) => update.byteLength > 0);\n if (updates.length === 0) {\n return undefined;\n }\n let version: DocVersionRecord = {};\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return {\n updates,\n version,\n };\n },\n applyRemoteUpdates: (updates, remoterVersion) => {\n assertDocAttached(doc);\n if (updates.length > 0) {\n doc.importBatch(updates);\n }\n let version = remoterVersion as DocVersionRecord;\n for (const update of updates) {\n const next = extractVersionFromUpdate(update);\n if (next != null) {\n version = mergeDocVersions(version, next);\n }\n }\n return version;\n },\n subscribeLocalUpdates: (listener) => {\n assertDocAttached(doc);\n return doc.subscribeLocalUpdates((update) => {\n const version = extractVersionFromUpdate(update) ?? {};\n listener({ updates: [update], version });\n });\n },\n };\n}\n"],"mappings":";;;AAcA,SAAS,mBAAmB,SAA0C;CACpE,MAAM,MAAM,EAAE;AACd,MAAK,MAAM,CAAC,MAAM,YAAY,QAAQ,QAAQ,CAC5C,KAAI,QAAkB;AAExB,QAAO;;AAGT,SAAS,gBAAgB,SAA0C;CACjE,MAAM,sBAAM,IAAI,KAAqB;AACrC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,QAAQ,CACnD,KAAI,OAAO,YAAY,YAAY,OAAO,SAAS,QAAQ,CACzD,KAAI,IAAI,MAAgB,QAAQ;AAGpC,QAAO;;AAGT,SAAS,iBACP,MACA,UACkB;CAClB,MAAM,yBAAS,IAAI,KAAqB;AACxC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,KAAK,CAChD,QAAO,IAAI,MAAgB,QAAQ;AAErC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,SAAS,EAAE;EACtD,MAAM,SAAS;AACf,SAAO,IAAI,QAAQ,KAAK,IAAI,OAAO,IAAI,OAAO,IAAI,GAAG,QAAQ,CAAC;;AAEhE,QAAO,mBAAmB,cAAc,UAAU,OAAO,CAAC;;AAG5D,SAAS,gBACP,aACA,gBACS;AACT,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,eAAe,CAC1D,KAAI,WAAW,YAAY,SAAmB,GAC5C,QAAO;AAGX,QAAO;;AAGT,SAAS,yBAAyB,QAA6C;AAC7E,KAAI;AAEF,SAAO,mBADM,qBAAqB,QAAQ,MAAM,CACjB,wBAAwB;SACjD;AACN,SAAO;;;AAIX,SAAS,kBAAkB,KAAoB;AAC7C,KAAI,CAAC,IAAI,YAAY,CACnB;AAEF,OAAM,IAAI,MACR,gGAED;;;;;;;;;;;;;;;;AAiBH,SAAgB,qBACd,KACyB;AACzB,QAAO;EACL,qBAAqB,EAAE;EACvB,gBAAgB,MAAM,aACpB,iBAAiB,MAA0B,SAA6B;EAC1E,sBAAsB;AACpB,qBAAkB,IAAI;AACtB,UAAO,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;;EAEzC,gBAAgB,aAAa;AAC3B,qBAAkB,IAAI;AACtB,OAAI,OAAO,SAAS;AACpB,UAAO,mBAAmB,IAAI,SAAS,CAAC;;EAE1C,gBAAgB,gBAAgB;AAC9B,qBAAkB,IAAI;AAEtB,OACE,CAAC,gBACC,aAHmB,mBAAmB,IAAI,SAAS,CAAC,CAKrD,CAED;GAEF,MAAM,UAAU,CACd,IAAI,OAAO;IACT,MAAM;IACN,MAAM,cAAc,UAAU,gBAAgB,YAAY,CAAC;IAC5D,CAAC,CACH,CAAC,QAAQ,WAAW,OAAO,aAAa,EAAE;AAC3C,OAAI,QAAQ,WAAW,EACrB;GAEF,IAAI,UAA4B,EAAE;AAClC,QAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,OAAO,yBAAyB,OAAO;AAC7C,QAAI,QAAQ,KACV,WAAU,iBAAiB,SAAS,KAAK;;AAG7C,UAAO;IACL;IACA;IACD;;EAEH,qBAAqB,SAAS,mBAAmB;AAC/C,qBAAkB,IAAI;AACtB,OAAI,QAAQ,SAAS,EACnB,KAAI,YAAY,QAAQ;GAE1B,IAAI,UAAU;AACd,QAAK,MAAM,UAAU,SAAS;IAC5B,MAAM,OAAO,yBAAyB,OAAO;AAC7C,QAAI,QAAQ,KACV,WAAU,iBAAiB,SAAS,KAAK;;AAG7C,UAAO;;EAET,wBAAwB,aAAa;AACnC,qBAAkB,IAAI;AACtB,UAAO,IAAI,uBAAuB,WAAW;IAC3C,MAAM,UAAU,yBAAyB,OAAO,IAAI,EAAE;AACtD,aAAS;KAAE,SAAS,CAAC,OAAO;KAAE;KAAS,CAAC;KACxC;;EAEL"}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
//#region src/checkpoint.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Persisted replay progress for one stream.
|
|
4
|
+
*
|
|
5
|
+
* This is metadata only — it does **not** contain any CRDT update data.
|
|
6
|
+
* A persisted cursor is only valid when the caller also restores the matching
|
|
7
|
+
* local CRDT state before the next `sync()` or `join()`.
|
|
8
|
+
*/
|
|
9
|
+
interface RemoteCursor<TVersion extends JsonObject = JsonObject> {
|
|
10
|
+
/** Identity of the stream this cursor belongs to. */
|
|
11
|
+
readonly streamUrl: string;
|
|
12
|
+
/** The offset to resume reading from on the next sync/join. */
|
|
13
|
+
readonly nextOffset: string;
|
|
14
|
+
/** The CRDT version that the server data up to `nextOffset` represents. */
|
|
15
|
+
readonly serverLowerBoundVersion: TVersion;
|
|
16
|
+
/** Timestamp (ms) when this cursor was last persisted. */
|
|
17
|
+
readonly updatedAtMs: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Storage for replay progress metadata.
|
|
21
|
+
*
|
|
22
|
+
* A persisted cursor is only valid when the caller also restores the matching
|
|
23
|
+
* local CRDT state before the next `sync()` or `join()`. Storing only the
|
|
24
|
+
* cursor can make transport resume from `nextOffset` against an empty local
|
|
25
|
+
* document, which is not sufficient for delta-only catchup.
|
|
26
|
+
*/
|
|
27
|
+
interface RemoteCursorStore<TVersion extends JsonObject = JsonObject> {
|
|
28
|
+
load(streamUrl: string): Promise<RemoteCursor<TVersion> | null>;
|
|
29
|
+
save(cursor: RemoteCursor<TVersion>): Promise<void>;
|
|
30
|
+
delete?(streamUrl: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Why the transport is advancing the remote cursor.
|
|
34
|
+
*/
|
|
35
|
+
type RemoteCursorSaveSource = "local" | "remote" | "bootstrap";
|
|
36
|
+
/**
|
|
37
|
+
* Context passed to durability hooks before a remote cursor is persisted.
|
|
38
|
+
*
|
|
39
|
+
* Hooks should make local durable state correspond to `cursor` before this
|
|
40
|
+
* cursor is stored, so a future process can restore the same CRDT state before
|
|
41
|
+
* transport resumes from `cursor.nextOffset`.
|
|
42
|
+
*/
|
|
43
|
+
type BeforeRemoteCursorSaveContext<TVersion extends JsonObject = JsonObject> = {
|
|
44
|
+
readonly cursor: RemoteCursor<TVersion>;
|
|
45
|
+
readonly source: RemoteCursorSaveSource;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Hook invoked before the transport persists replay progress.
|
|
49
|
+
*
|
|
50
|
+
* This is the place to persist local CRDT payloads or snapshots. If that local
|
|
51
|
+
* durability step does not happen here, the remote cursor should not advance.
|
|
52
|
+
*/
|
|
53
|
+
type BeforeRemoteCursorSaveHook<TVersion extends JsonObject = JsonObject> = (context: BeforeRemoteCursorSaveContext<TVersion>) => void | Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Construction options for `IndexedDbRemoteCursorStore`.
|
|
56
|
+
*/
|
|
57
|
+
interface IndexedDbRemoteCursorStoreOptions {
|
|
58
|
+
readonly dbName?: string;
|
|
59
|
+
readonly storeName?: string;
|
|
60
|
+
readonly dbVersion?: number;
|
|
61
|
+
readonly indexedDb?: IDBFactory;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* In-memory cursor store useful for tests or ephemeral runtimes.
|
|
65
|
+
*/
|
|
66
|
+
declare class InMemoryRemoteCursorStore<TVersion extends JsonObject = JsonObject> implements RemoteCursorStore<TVersion> {
|
|
67
|
+
private readonly data;
|
|
68
|
+
load(streamUrl: string): Promise<RemoteCursor<TVersion> | null>;
|
|
69
|
+
save(cursor: RemoteCursor<TVersion>): Promise<void>;
|
|
70
|
+
delete(streamUrl: string): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* IndexedDB-backed cursor store for browser clients that need replay progress persistence.
|
|
74
|
+
*
|
|
75
|
+
* This store only persists `RemoteCursor` metadata. It does not persist CRDT
|
|
76
|
+
* payloads. Use it only when the application also persists the corresponding
|
|
77
|
+
* local document state and restores that state before calling `sync()` or
|
|
78
|
+
* `join()` after a reload.
|
|
79
|
+
*/
|
|
80
|
+
declare class IndexedDbRemoteCursorStore<TVersion extends JsonObject = JsonObject> implements RemoteCursorStore<TVersion> {
|
|
81
|
+
private readonly dbName;
|
|
82
|
+
private readonly storeName;
|
|
83
|
+
private readonly dbVersion;
|
|
84
|
+
private readonly indexedDb;
|
|
85
|
+
private db;
|
|
86
|
+
private initPromise;
|
|
87
|
+
constructor(options?: IndexedDbRemoteCursorStoreOptions);
|
|
88
|
+
load(streamUrl: string): Promise<RemoteCursor<TVersion> | null>;
|
|
89
|
+
save(cursor: RemoteCursor<TVersion>): Promise<void>;
|
|
90
|
+
delete(streamUrl: string): Promise<void>;
|
|
91
|
+
private ensureDb;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates a cursor value suitable for seeding a `RemoteCursorStore`.
|
|
95
|
+
*/
|
|
96
|
+
declare function createInitialRemoteCursor<TVersion extends JsonObject = JsonObject>(input: {
|
|
97
|
+
streamUrl: string;
|
|
98
|
+
nextOffset: string;
|
|
99
|
+
serverLowerBoundVersion: TVersion;
|
|
100
|
+
}): RemoteCursor<TVersion>;
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/reconnect.d.ts
|
|
103
|
+
/**
|
|
104
|
+
* Retry and timeout settings used by `StreamsCrdt`.
|
|
105
|
+
*/
|
|
106
|
+
interface ReconnectConfig {
|
|
107
|
+
readonly delays: readonly number[];
|
|
108
|
+
readonly jitterFraction: number;
|
|
109
|
+
readonly maxAttempts: number;
|
|
110
|
+
readonly connectTimeoutMs: number;
|
|
111
|
+
readonly pollTimeoutMs: number;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/types.d.ts
|
|
115
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
116
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
117
|
+
/**
|
|
118
|
+
* JSON-compatible value type used by transport metadata such as versions and cursors.
|
|
119
|
+
*/
|
|
120
|
+
type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
121
|
+
/**
|
|
122
|
+
* JSON object shape used by public transport types.
|
|
123
|
+
*/
|
|
124
|
+
type JsonObject = {
|
|
125
|
+
readonly [key: string]: JsonValue;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Authorization source for gateway requests.
|
|
129
|
+
*/
|
|
130
|
+
type StreamsAuthProvider = string | (() => MaybePromise<string | null | undefined>);
|
|
131
|
+
/**
|
|
132
|
+
* Rust-style result for transport operations that may fail for expected reasons.
|
|
133
|
+
*/
|
|
134
|
+
type Result<T, E> = {
|
|
135
|
+
readonly ok: true;
|
|
136
|
+
readonly value: T;
|
|
137
|
+
} | {
|
|
138
|
+
readonly ok: false;
|
|
139
|
+
readonly error: E;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Live subscription state exposed by `join()`.
|
|
143
|
+
*/
|
|
144
|
+
type TransportRoomStatus = "joined" | "reconnecting" | "disconnected" | "error";
|
|
145
|
+
/**
|
|
146
|
+
* Discriminated error union for `createStream()`, `deleteStream()`, `sync()`, and `join()`.
|
|
147
|
+
*
|
|
148
|
+
* - `stream_not_found` — the target stream does not exist (404).
|
|
149
|
+
* - `auth_failed` — the auth token was rejected (401/403).
|
|
150
|
+
* - `protocol_error` — unexpected server response or framing error.
|
|
151
|
+
* - `network_error` — fetch-level failure (DNS, connection refused, etc.).
|
|
152
|
+
* - `timeout` — connect or poll deadline exceeded.
|
|
153
|
+
* - `gone` — the server indicated the stream was deleted or moved (410).
|
|
154
|
+
* - `internal_error` — adapter or runtime logic failed locally.
|
|
155
|
+
* - `unknown` — catch-all for unexpected errors.
|
|
156
|
+
*/
|
|
157
|
+
type TransportError = {
|
|
158
|
+
readonly code: "stream_not_found";
|
|
159
|
+
readonly retryable: false;
|
|
160
|
+
} | {
|
|
161
|
+
readonly code: "auth_failed";
|
|
162
|
+
readonly retryable: false;
|
|
163
|
+
readonly status?: 401 | 403;
|
|
164
|
+
readonly message: string;
|
|
165
|
+
} | {
|
|
166
|
+
readonly code: "protocol_error";
|
|
167
|
+
readonly retryable: false;
|
|
168
|
+
readonly status?: number;
|
|
169
|
+
readonly message: string;
|
|
170
|
+
} | {
|
|
171
|
+
readonly code: "network_error";
|
|
172
|
+
readonly retryable: true;
|
|
173
|
+
readonly message: string;
|
|
174
|
+
} | {
|
|
175
|
+
readonly code: "timeout";
|
|
176
|
+
readonly retryable: true;
|
|
177
|
+
readonly phase: "connect" | "poll";
|
|
178
|
+
readonly message: string;
|
|
179
|
+
} | {
|
|
180
|
+
readonly code: "gone";
|
|
181
|
+
readonly retryable: true;
|
|
182
|
+
readonly message: string;
|
|
183
|
+
} | {
|
|
184
|
+
readonly code: "internal_error";
|
|
185
|
+
readonly retryable: false;
|
|
186
|
+
readonly message: string;
|
|
187
|
+
} | {
|
|
188
|
+
readonly code: "unknown";
|
|
189
|
+
readonly retryable: false;
|
|
190
|
+
readonly message: string;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Result payload returned by `createStream()`.
|
|
194
|
+
*/
|
|
195
|
+
interface TransportCreateStreamSuccess {
|
|
196
|
+
readonly created: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Result payload returned by `deleteStream()`.
|
|
200
|
+
*/
|
|
201
|
+
interface TransportDeleteStreamSuccess {
|
|
202
|
+
readonly deleted: boolean;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Result payload returned by `sync()`.
|
|
206
|
+
*/
|
|
207
|
+
interface TransportSyncSuccess<TVersion extends JsonObject = JsonObject> {
|
|
208
|
+
readonly cursor: RemoteCursor<TVersion>;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Optional parameters for a `join()` call.
|
|
212
|
+
*
|
|
213
|
+
* `join()` performs an initial sync automatically before entering live mode.
|
|
214
|
+
* There is no need to call `sync()` before `join()`.
|
|
215
|
+
*/
|
|
216
|
+
interface TransportJoinParams {
|
|
217
|
+
/**
|
|
218
|
+
* Called when the live subscription status changes.
|
|
219
|
+
*
|
|
220
|
+
* Status flow: `"connecting"` → `"joined"` → (on error) `"reconnecting"` →
|
|
221
|
+
* `"joined"` or `"disconnected"` or `"error"`.
|
|
222
|
+
*/
|
|
223
|
+
readonly onStatusChange?: (status: "connecting" | TransportRoomStatus) => void;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Controls best-effort snapshot uploads scheduled from local writes during `join()`.
|
|
227
|
+
*
|
|
228
|
+
* Snapshots are only uploaded during `join()`, never `sync()`. A debounce
|
|
229
|
+
* window (default 10 s) is refreshed on each local write. If another writer
|
|
230
|
+
* advances the remote version during that window, the upload is skipped.
|
|
231
|
+
*/
|
|
232
|
+
interface SnapshotUploadOptions {
|
|
233
|
+
/** Master switch. Defaults to `true` when `snapshotUpload` is provided. */
|
|
234
|
+
readonly enabled?: boolean;
|
|
235
|
+
/** Authorization gate — called before each upload attempt. */
|
|
236
|
+
readonly canUpload: () => MaybePromise<boolean>;
|
|
237
|
+
/** Debounce window in milliseconds after the last local write. Defaults to 10 000. */
|
|
238
|
+
readonly debounceMs?: number;
|
|
239
|
+
/** Minimum byte delta since the last remote snapshot before uploading. */
|
|
240
|
+
readonly minBytesSinceRemoteSnapshot?: number;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Active live subscription returned by `join()`.
|
|
244
|
+
*
|
|
245
|
+
* The subscription keeps the SSE/long-poll connection alive and forwards
|
|
246
|
+
* local writes to the server. Call `unsubscribe()` to leave the room.
|
|
247
|
+
*/
|
|
248
|
+
interface TransportSubscription {
|
|
249
|
+
/** Leaves the live room and stops all background I/O for this subscription. */
|
|
250
|
+
readonly unsubscribe: () => void;
|
|
251
|
+
/** Whether the underlying connection is currently open. */
|
|
252
|
+
readonly connected: boolean;
|
|
253
|
+
/** Current room status. */
|
|
254
|
+
readonly status: TransportRoomStatus;
|
|
255
|
+
/** Registers a status listener. Returns an unsubscribe function. */
|
|
256
|
+
readonly onStatusChange: (listener: (status: "connecting" | TransportRoomStatus) => void) => () => void;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Local CRDT updates ready to append to Durable Streams.
|
|
260
|
+
*/
|
|
261
|
+
interface CrdtUpdateBatch<TVersion extends JsonObject> {
|
|
262
|
+
readonly updates: readonly Uint8Array[];
|
|
263
|
+
readonly version: TVersion;
|
|
264
|
+
}
|
|
265
|
+
type ExportedCrdtUpdateBatch<TVersion extends JsonObject> = CrdtUpdateBatch<TVersion> | null | undefined;
|
|
266
|
+
/**
|
|
267
|
+
* Adapter contract between `StreamsCrdt` and a concrete CRDT runtime.
|
|
268
|
+
*/
|
|
269
|
+
interface CrdtAdapter<TVersion extends JsonObject> {
|
|
270
|
+
/** Returns the zero/empty version for this CRDT type. */
|
|
271
|
+
emptyVersion(): TVersion;
|
|
272
|
+
/** Returns the element-wise maximum of two versions. Must be idempotent and commutative. */
|
|
273
|
+
mergeVersions(base: TVersion, incoming: TVersion): TVersion;
|
|
274
|
+
/** Exports a full snapshot of the current local CRDT state. */
|
|
275
|
+
exportSnapshot(): Uint8Array;
|
|
276
|
+
/** Imports a full snapshot, replacing local state. Returns the version after import. */
|
|
277
|
+
applySnapshot(snapshot: Uint8Array): MaybePromise<TVersion>;
|
|
278
|
+
/** Exports incremental updates since `fromVersion`. Returns `null`/`undefined` when there are no new updates. */
|
|
279
|
+
exportUpdates(fromVersion: TVersion): ExportedCrdtUpdateBatch<TVersion>;
|
|
280
|
+
/**
|
|
281
|
+
* Applies remote updates against the local CRDT state.
|
|
282
|
+
*
|
|
283
|
+
* `remoterVersion` is the current inferred remote version before applying
|
|
284
|
+
* `updates`. The return value must be the new inferred remote version after
|
|
285
|
+
* these updates have been applied.
|
|
286
|
+
*
|
|
287
|
+
* Contract: this function must be stable for self-replay. If `updates` only
|
|
288
|
+
* contain data that is already represented by `remoterVersion` (for example a
|
|
289
|
+
* transport replay of this writer's own successful append), the returned
|
|
290
|
+
* version must compare equal to `remoterVersion`. Transport relies on that
|
|
291
|
+
* invariant to distinguish self-replay from another writer advancing the
|
|
292
|
+
* remote version.
|
|
293
|
+
*/
|
|
294
|
+
applyRemoteUpdates(updates: Uint8Array[], remoterVersion: TVersion): MaybePromise<TVersion>;
|
|
295
|
+
/** Subscribes to local CRDT mutations. Returns an unsubscribe function. */
|
|
296
|
+
subscribeLocalUpdates(listener: (batch: CrdtUpdateBatch<TVersion>) => void): () => void;
|
|
297
|
+
/** Optional headers to include when creating the stream (e.g. retention config). */
|
|
298
|
+
getCreateStreamHeaders?(): MaybePromise<Record<string, string>>;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Construction options for one `StreamsCrdt` instance.
|
|
302
|
+
*
|
|
303
|
+
* Each instance is bound to exactly one stream URL and one local CRDT adapter.
|
|
304
|
+
*/
|
|
305
|
+
interface StreamsCrdtOptions<TVersion extends JsonObject> {
|
|
306
|
+
/** Full stream URL, typically built with `createStreamUrl()`. */
|
|
307
|
+
readonly streamUrl: string;
|
|
308
|
+
/** CRDT adapter for the local document or replica. */
|
|
309
|
+
readonly adapter: CrdtAdapter<TVersion>;
|
|
310
|
+
/**
|
|
311
|
+
* When true, the initial sync/join bootstrap may create the stream on a 404 before retrying.
|
|
312
|
+
* Defaults to false so missing streams are surfaced explicitly.
|
|
313
|
+
*/
|
|
314
|
+
readonly createStreamIfMissing?: boolean;
|
|
315
|
+
readonly auth?: StreamsAuthProvider;
|
|
316
|
+
readonly fetch?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
317
|
+
/**
|
|
318
|
+
* Stores replay progress for this stream.
|
|
319
|
+
*
|
|
320
|
+
* Persist this only when the application also persists the matching local
|
|
321
|
+
* CRDT state and restores it before the next `sync()` or `join()`. Otherwise
|
|
322
|
+
* transport may resume from deltas against an empty local document.
|
|
323
|
+
*/
|
|
324
|
+
readonly remoteCursorStore?: RemoteCursorStore<TVersion>;
|
|
325
|
+
/**
|
|
326
|
+
* Runs immediately before the remote cursor is persisted.
|
|
327
|
+
*
|
|
328
|
+
* Use this to durably persist the local CRDT payload or snapshot that
|
|
329
|
+
* corresponds to the cursor being saved.
|
|
330
|
+
*/
|
|
331
|
+
readonly beforeRemoteCursorSave?: BeforeRemoteCursorSaveHook<TVersion>;
|
|
332
|
+
/**
|
|
333
|
+
* Same as `beforeRemoteCursorSave`, but allows composing multiple durability
|
|
334
|
+
* hooks.
|
|
335
|
+
*/
|
|
336
|
+
readonly beforeRemoteCursorSaveHooks?: ReadonlyArray<BeforeRemoteCursorSaveHook<TVersion>>;
|
|
337
|
+
readonly snapshotUpload?: SnapshotUploadOptions;
|
|
338
|
+
readonly reconnectConfig?: Partial<ReconnectConfig>;
|
|
339
|
+
readonly debug?: boolean;
|
|
340
|
+
}
|
|
341
|
+
interface StreamsCrdtLike<TVersion extends JsonObject> {
|
|
342
|
+
createStream(): Promise<Result<TransportCreateStreamSuccess, TransportError>>;
|
|
343
|
+
deleteStream(): Promise<Result<TransportDeleteStreamSuccess, TransportError>>;
|
|
344
|
+
sync(): Promise<Result<TransportSyncSuccess<TVersion>, TransportError>>;
|
|
345
|
+
join(params?: TransportJoinParams): Promise<Result<TransportSubscription, TransportError>>;
|
|
346
|
+
close(): Promise<void>;
|
|
347
|
+
getSnapshotOffset(): Promise<Result<string | null, TransportError>>;
|
|
348
|
+
readonly streamUrl: string;
|
|
349
|
+
readonly adapter: CrdtAdapter<TVersion>;
|
|
350
|
+
readonly remoteCursorStore: RemoteCursorStore<TVersion>;
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/transport-runtime.d.ts
|
|
354
|
+
/**
|
|
355
|
+
* Durable Streams transport runtime bound to one stream URL and one local CRDT adapter.
|
|
356
|
+
*/
|
|
357
|
+
declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLike<TVersion> {
|
|
358
|
+
readonly streamUrl: string;
|
|
359
|
+
readonly adapter: CrdtAdapter<TVersion>;
|
|
360
|
+
readonly remoteCursorStore: RemoteCursorStore<TVersion>;
|
|
361
|
+
private readonly createStreamIfMissing;
|
|
362
|
+
private readonly client;
|
|
363
|
+
private readonly cursorManager;
|
|
364
|
+
private readonly reconnectConfig;
|
|
365
|
+
private readonly debug;
|
|
366
|
+
private readonly beforeRemoteCursorSave?;
|
|
367
|
+
private readonly snapshotManager;
|
|
368
|
+
private readonly producerId;
|
|
369
|
+
private producerEpoch;
|
|
370
|
+
private nextProducerSeq;
|
|
371
|
+
private joinState?;
|
|
372
|
+
private opQueue;
|
|
373
|
+
/**
|
|
374
|
+
* Creates a transport for one stream URL and one local adapter instance.
|
|
375
|
+
*/
|
|
376
|
+
constructor(options: StreamsCrdtOptions<TVersion>);
|
|
377
|
+
/**
|
|
378
|
+
* Stops any active live join and releases transport-owned state.
|
|
379
|
+
*/
|
|
380
|
+
close(): Promise<void>;
|
|
381
|
+
/**
|
|
382
|
+
* Reads the latest remote snapshot offset, if the server exposes one.
|
|
383
|
+
*/
|
|
384
|
+
getSnapshotOffset(): Promise<Result<string | null, TransportError>>;
|
|
385
|
+
/**
|
|
386
|
+
* Explicitly creates the target stream.
|
|
387
|
+
*/
|
|
388
|
+
createStream(): Promise<Result<TransportCreateStreamSuccess, TransportError>>;
|
|
389
|
+
/**
|
|
390
|
+
* Deletes the target stream and clears persisted replay state when supported by the cursor store.
|
|
391
|
+
*/
|
|
392
|
+
deleteStream(): Promise<Result<TransportDeleteStreamSuccess, TransportError>>;
|
|
393
|
+
/**
|
|
394
|
+
* Runs one bootstrap/catch-up cycle without entering live mode.
|
|
395
|
+
*/
|
|
396
|
+
sync(): Promise<Result<TransportSyncSuccess<TVersion>, TransportError>>;
|
|
397
|
+
/**
|
|
398
|
+
* Replays remote state, flushes queued local updates, then enters live read mode.
|
|
399
|
+
*/
|
|
400
|
+
join(params?: TransportJoinParams): Promise<Result<TransportSubscription, TransportError>>;
|
|
401
|
+
private createSubscription;
|
|
402
|
+
private disposeJoinState;
|
|
403
|
+
private startJoin;
|
|
404
|
+
private syncOnce;
|
|
405
|
+
private performInitialJoinSync;
|
|
406
|
+
private bootstrapIntoState;
|
|
407
|
+
private bootstrapIntoJoinedState;
|
|
408
|
+
private bootstrapIntoJoinedStateViaSse;
|
|
409
|
+
private continueJoinedSseSessionUntilCaughtUp;
|
|
410
|
+
private bootstrapIntoJoinedStateViaLongPoll;
|
|
411
|
+
private catchup;
|
|
412
|
+
private appendLocalBatch;
|
|
413
|
+
private exportUpdates;
|
|
414
|
+
private applyBootstrapItems;
|
|
415
|
+
private applyRemoteItems;
|
|
416
|
+
private runReadLoop;
|
|
417
|
+
/** Maximum encoded size (bytes) for a single batched append. */
|
|
418
|
+
private static readonly MAX_BATCH_BYTES;
|
|
419
|
+
/**
|
|
420
|
+
* Drain up to `MAX_BATCH_BYTES` worth of queued batches from the front of
|
|
421
|
+
* `pendingLocal` and merge them into a single `CrdtUpdateBatch`.
|
|
422
|
+
*
|
|
423
|
+
* Returns `undefined` when the queue is empty.
|
|
424
|
+
*/
|
|
425
|
+
private drainPendingBatch;
|
|
426
|
+
private flushPendingLocal;
|
|
427
|
+
private saveRemoteCursor;
|
|
428
|
+
private setJoinStatus;
|
|
429
|
+
private createStreamRequest;
|
|
430
|
+
private deleteStreamRequest;
|
|
431
|
+
private readFromOffsetSseItems;
|
|
432
|
+
private openBootstrapSseSession;
|
|
433
|
+
private readBootstrapLongPoll;
|
|
434
|
+
private readFromOffset;
|
|
435
|
+
private bootstrap;
|
|
436
|
+
private readSnapshotOffsetValue;
|
|
437
|
+
private putSnapshotAt;
|
|
438
|
+
private appendUpdates;
|
|
439
|
+
private enqueueExclusive;
|
|
440
|
+
private logDebug;
|
|
441
|
+
private logError;
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/stream-id.d.ts
|
|
445
|
+
/**
|
|
446
|
+
* Validates a stream id accepted by `createStreamUrl()`.
|
|
447
|
+
*/
|
|
448
|
+
declare function isValidRillId(id: string): boolean;
|
|
449
|
+
/**
|
|
450
|
+
* Validates a bucket id accepted by `createStreamUrl()`.
|
|
451
|
+
*/
|
|
452
|
+
declare function isValidBucketId(id: string): boolean;
|
|
453
|
+
/**
|
|
454
|
+
* Builds a Durable Streams HTTP URL for one `(bucketId, streamId)` pair.
|
|
455
|
+
*/
|
|
456
|
+
declare function createStreamUrl(input: {
|
|
457
|
+
bucketId: string;
|
|
458
|
+
streamId: string;
|
|
459
|
+
baseUrl?: string;
|
|
460
|
+
}): string;
|
|
461
|
+
//#endregion
|
|
462
|
+
export { IndexedDbRemoteCursorStore as C, RemoteCursorStore as D, RemoteCursorSaveSource as E, createInitialRemoteCursor as O, InMemoryRemoteCursorStore as S, RemoteCursor as T, TransportRoomStatus as _, CrdtAdapter as a, BeforeRemoteCursorSaveContext as b, JsonValue as c, StreamsAuthProvider as d, StreamsCrdtOptions as f, TransportJoinParams as g, TransportError as h, StreamsCrdt as i, Result as l, TransportDeleteStreamSuccess as m, isValidBucketId as n, CrdtUpdateBatch as o, TransportCreateStreamSuccess as p, isValidRillId as r, JsonObject as s, createStreamUrl as t, SnapshotUploadOptions as u, TransportSubscription as v, IndexedDbRemoteCursorStoreOptions as w, BeforeRemoteCursorSaveHook as x, TransportSyncSuccess as y };
|
|
463
|
+
//# sourceMappingURL=stream-id-1f2mhtmk.d.ts.map
|