@deepfates/lync 0.3.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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/bin/lync.js +2 -0
  4. package/dist/cli/bin.d.ts +3 -0
  5. package/dist/cli/bin.d.ts.map +1 -0
  6. package/dist/cli/bin.js +5 -0
  7. package/dist/cli/bin.js.map +1 -0
  8. package/dist/cli/index.d.ts +11 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +404 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/sync.d.ts +42 -0
  13. package/dist/cli/sync.d.ts.map +1 -0
  14. package/dist/cli/sync.js +232 -0
  15. package/dist/cli/sync.js.map +1 -0
  16. package/dist/client/create.d.ts +10 -0
  17. package/dist/client/create.d.ts.map +1 -0
  18. package/dist/client/create.js +50 -0
  19. package/dist/client/create.js.map +1 -0
  20. package/dist/client/index.d.ts +3 -0
  21. package/dist/client/index.d.ts.map +1 -0
  22. package/dist/client/index.js +3 -0
  23. package/dist/client/index.js.map +1 -0
  24. package/dist/client/testing.d.ts +14 -0
  25. package/dist/client/testing.d.ts.map +1 -0
  26. package/dist/client/testing.js +19 -0
  27. package/dist/client/testing.js.map +1 -0
  28. package/dist/client/types.d.ts +49 -0
  29. package/dist/client/types.d.ts.map +1 -0
  30. package/dist/client/types.js +2 -0
  31. package/dist/client/types.js.map +1 -0
  32. package/dist/errors.d.ts +16 -0
  33. package/dist/errors.d.ts.map +1 -0
  34. package/dist/errors.js +39 -0
  35. package/dist/errors.js.map +1 -0
  36. package/dist/events.d.ts +119 -0
  37. package/dist/events.d.ts.map +1 -0
  38. package/dist/events.js +686 -0
  39. package/dist/events.js.map +1 -0
  40. package/dist/file-log.d.ts +25 -0
  41. package/dist/file-log.d.ts.map +1 -0
  42. package/dist/file-log.js +97 -0
  43. package/dist/file-log.js.map +1 -0
  44. package/dist/idb-log.d.ts +21 -0
  45. package/dist/idb-log.d.ts.map +1 -0
  46. package/dist/idb-log.js +114 -0
  47. package/dist/idb-log.js.map +1 -0
  48. package/dist/index.d.ts +9 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +11 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/indexes/entries.d.ts +6 -0
  53. package/dist/indexes/entries.d.ts.map +1 -0
  54. package/dist/indexes/entries.js +7 -0
  55. package/dist/indexes/entries.js.map +1 -0
  56. package/dist/indexes/index.d.ts +3 -0
  57. package/dist/indexes/index.d.ts.map +1 -0
  58. package/dist/indexes/index.js +3 -0
  59. package/dist/indexes/index.js.map +1 -0
  60. package/dist/indexes/memory.d.ts +4 -0
  61. package/dist/indexes/memory.d.ts.map +1 -0
  62. package/dist/indexes/memory.js +193 -0
  63. package/dist/indexes/memory.js.map +1 -0
  64. package/dist/indexes/types.d.ts +70 -0
  65. package/dist/indexes/types.d.ts.map +1 -0
  66. package/dist/indexes/types.js +2 -0
  67. package/dist/indexes/types.js.map +1 -0
  68. package/dist/json.d.ts +3 -0
  69. package/dist/json.d.ts.map +1 -0
  70. package/dist/json.js +15 -0
  71. package/dist/json.js.map +1 -0
  72. package/dist/looms.d.ts +25 -0
  73. package/dist/looms.d.ts.map +1 -0
  74. package/dist/looms.js +359 -0
  75. package/dist/looms.js.map +1 -0
  76. package/dist/memory-log.d.ts +5 -0
  77. package/dist/memory-log.d.ts.map +1 -0
  78. package/dist/memory-log.js +7 -0
  79. package/dist/memory-log.js.map +1 -0
  80. package/dist/memory.d.ts +4 -0
  81. package/dist/memory.d.ts.map +1 -0
  82. package/dist/memory.js +255 -0
  83. package/dist/memory.js.map +1 -0
  84. package/dist/profiles/text-story.d.ts +26 -0
  85. package/dist/profiles/text-story.d.ts.map +1 -0
  86. package/dist/profiles/text-story.js +55 -0
  87. package/dist/profiles/text-story.js.map +1 -0
  88. package/dist/references.d.ts +14 -0
  89. package/dist/references.d.ts.map +1 -0
  90. package/dist/references.js +109 -0
  91. package/dist/references.js.map +1 -0
  92. package/dist/relay/attach.d.ts +24 -0
  93. package/dist/relay/attach.d.ts.map +1 -0
  94. package/dist/relay/attach.js +54 -0
  95. package/dist/relay/attach.js.map +1 -0
  96. package/dist/relay/index.d.ts +4 -0
  97. package/dist/relay/index.d.ts.map +1 -0
  98. package/dist/relay/index.js +4 -0
  99. package/dist/relay/index.js.map +1 -0
  100. package/dist/relay/relay.d.ts +80 -0
  101. package/dist/relay/relay.d.ts.map +1 -0
  102. package/dist/relay/relay.js +329 -0
  103. package/dist/relay/relay.js.map +1 -0
  104. package/dist/relay/serve.d.ts +18 -0
  105. package/dist/relay/serve.d.ts.map +1 -0
  106. package/dist/relay/serve.js +37 -0
  107. package/dist/relay/serve.js.map +1 -0
  108. package/dist/sha256.d.ts +2 -0
  109. package/dist/sha256.d.ts.map +1 -0
  110. package/dist/sha256.js +84 -0
  111. package/dist/sha256.js.map +1 -0
  112. package/dist/store.d.ts +97 -0
  113. package/dist/store.d.ts.map +1 -0
  114. package/dist/store.js +250 -0
  115. package/dist/store.js.map +1 -0
  116. package/dist/sync-protocol.d.ts +80 -0
  117. package/dist/sync-protocol.d.ts.map +1 -0
  118. package/dist/sync-protocol.js +135 -0
  119. package/dist/sync-protocol.js.map +1 -0
  120. package/dist/synced-store.d.ts +68 -0
  121. package/dist/synced-store.d.ts.map +1 -0
  122. package/dist/synced-store.js +340 -0
  123. package/dist/synced-store.js.map +1 -0
  124. package/dist/types.d.ts +78 -0
  125. package/dist/types.d.ts.map +1 -0
  126. package/dist/types.js +2 -0
  127. package/dist/types.js.map +1 -0
  128. package/dist/uuid.d.ts +9 -0
  129. package/dist/uuid.d.ts.map +1 -0
  130. package/dist/uuid.js +25 -0
  131. package/dist/uuid.js.map +1 -0
  132. package/dist/views.d.ts +78 -0
  133. package/dist/views.d.ts.map +1 -0
  134. package/dist/views.js +192 -0
  135. package/dist/views.js.map +1 -0
  136. package/package.json +183 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 deepfates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # lync
2
+
3
+ Most software forgets. Edit a document and yesterday's version is gone. Write
4
+ with an AI that offers three options and the two you do not pick vanish. Let a
5
+ tool merge two people's edits and you get one result with no memory of who did
6
+ what or what was dropped. The story of how a thing came to be is thrown away at
7
+ every step.
8
+
9
+ lync keeps it. A `.lync` file is a list of events, one per line, in plain text.
10
+ Each line records one thing that happened. Someone wrote a sentence, marked a
11
+ version as good, or chose one branch over another. You never edit a line. To
12
+ change something you add a new line that points at the old one and says what
13
+ changed. The old line stays.
14
+
15
+ That one rule, only ever add and never edit, is where everything good comes
16
+ from. You can always see the whole history, every version and every branch you
17
+ did not take. Merging two copies is safe and boring, because you keep every
18
+ event from both and there are no edits to fight over. If the same event ever
19
+ shows up two different ways, lync keeps both and says so out loud instead of
20
+ quietly picking one. And because the file is plain text with a written-down
21
+ spec, you can still read it in twenty years, on a computer that has never heard
22
+ of this software.
23
+
24
+ A lync file is more than a transcript, because it keeps the branches and the
25
+ choices. From the same file you can make a readable document to hand someone,
26
+ or training data built from the exact versions you marked as good.
27
+
28
+ `@deepfates/lync` is the reference implementation. One package, zero runtime
29
+ dependencies, holding the parser, the event stores, the computed views, the
30
+ loom API, live sync, the `lync` command, and the sync relay. The format is the
31
+ durable center. Everything else is a tool that reads and writes it.
32
+
33
+ ## The Format
34
+
35
+ The normative specification is [FORMAT.md](./FORMAT.md). It is self-contained:
36
+ no code in this repository is required to implement it.
37
+
38
+ The short version:
39
+
40
+ - A `.lync` file is UTF-8 JSONL, no BOM, one event object per LF-terminated
41
+ line.
42
+ - The required event fields are `v`, `id`, `kind`, `at`, `author`, `parents`,
43
+ and `payload`.
44
+ - `author.actor` is the content producer; `author.via` is the app or runtime;
45
+ converters use `imported_by` and preserve the original actor.
46
+ - Events are immutable. Correction, judgment, selection, and retraction are new
47
+ events that point at prior events.
48
+ - Merge is union by event id. Same id plus same body bytes is one event seen
49
+ twice; same id plus different body bytes is a surfaced conflict variant and is
50
+ excluded from ordinary views.
51
+ - Stored line metadata may splice `digest` and `sig` at the end of the line.
52
+ `digest` and `sig` are reserved top-level body names; payloads may use those
53
+ names freely.
54
+ - Views are computed. This package ships branch tree, transcript, memory, and
55
+ leaderboard helpers.
56
+
57
+ Every line has the same envelope:
58
+
59
+ ```json
60
+ {"v":1,"id":"root","kind":"notes/text","at":"2026-07-06T04:12:31Z","author":{"actor":"deepfates","via":"example@0.1"},"parents":[],"payload":{"text":"Once..."}}
61
+ ```
62
+
63
+ That line can be copied to another file, merged back later, verified byte for
64
+ byte, and read by software that has never heard of `notes/text`. Unknown
65
+ kinds are carried and traversed; meaning belongs to pacts layered above the
66
+ format — see [pacts/import.md](./pacts/import.md) (imports are transcription:
67
+ deterministic ids, provenance preserved, zero silent drops) and
68
+ [pacts/export.md](./pacts/export.md) (exports are projections of the event
69
+ log, including training data).
70
+
71
+ ## Conformance Vectors
72
+
73
+ The format is meant to be implemented in other languages, and the test
74
+ vectors are the product that makes a port checkable:
75
+ [test/vectors/v0](./test/vectors/v0). Each case is a directory holding a raw
76
+ input file (`input.lync`, or `a.lync` + `b.lync` for the merge case) and an
77
+ `expected.json` with the required classification of every physical line —
78
+ accepted, nonconforming, garbage, damaged, conflict-variant — plus union ids,
79
+ pending parents, and graph diagnostics. The thirteen cases cover valid events,
80
+ splice anchoring, damaged digests, garbage classes, same-id conflicts and
81
+ duplicates, graph obstacles, critical suppression, spelling-versus-value
82
+ equality, `marked`/`at` semantics, merge union, carried nonconforming lines,
83
+ and invalid signature splices.
84
+
85
+ A new implementation ports the vector suite first, this package's test suite
86
+ second, and its design never. `test/vectors/v0/README.md` documents the
87
+ expected-output schema; `generate.py` regenerates digests deterministically.
88
+
89
+ ## The Library
90
+
91
+ ```bash
92
+ npm install @deepfates/lync
93
+ ```
94
+
95
+ Runs in Node (>=22) and the browser. No dependencies.
96
+
97
+ ### Parse, union, view
98
+
99
+ ```ts
100
+ import { parseLyncFiles } from "@deepfates/lync/events";
101
+ import { lyncBranchTreeView, lyncTranscriptView } from "@deepfates/lync/views";
102
+
103
+ const bytes = new TextEncoder().encode(
104
+ '{"v":1,"id":"root","kind":"notes/text","at":"2026-07-06T04:12:31Z","author":{"actor":"you"},"parents":[],"payload":{"text":"Once..."}}\n',
105
+ );
106
+
107
+ const parsed = parseLyncFiles([{ file: "story.lync", bytes }]);
108
+ console.log(parsed.lines[0].class); // "accepted"
109
+ console.log(lyncBranchTreeView(parsed).roots);
110
+ console.log(lyncTranscriptView(parsed, "root").entries.map((entry) => entry.id));
111
+ ```
112
+
113
+ `parseLyncFiles` classifies every physical line and keeps the original bytes —
114
+ accepted events, nonconforming-but-carried lines, damaged lines, garbage, and
115
+ conflict variants (same id, different bytes) are all preserved and reported,
116
+ never silently dropped. `exportCarriedLyncBytes(parsed)` re-emits the carried
117
+ bytes. `LyncUnion` performs the same union incrementally and can buffer
118
+ children until their first missing parent arrives.
119
+
120
+ ### Stores and looms
121
+
122
+ Event stores share one contract over memory, file, and IndexedDB backends. The
123
+ loom API gives programs turns and threads instead of raw events, on top of any
124
+ store:
125
+
126
+ ```ts
127
+ import { createLyncLooms } from "@deepfates/lync/looms";
128
+ import { createMemoryEventStore } from "@deepfates/lync/memory-log";
129
+
130
+ const looms = createLyncLooms({
131
+ store: createMemoryEventStore(),
132
+ author: { actor: "you", via: "my-app@0.1" },
133
+ });
134
+
135
+ const info = await looms.create({ title: "Story" });
136
+ const loom = await looms.open(info.id);
137
+ const first = await loom.appendTurn(null, { text: "Once..." });
138
+ await loom.appendTurn(first.id, { text: "Then..." });
139
+ ```
140
+
141
+ The store API accepts raw lines through `union(line)` and structured event
142
+ bodies through `append(event)`. It reports conflicts, pending parents, garbage,
143
+ and accepted events without making file order meaningful.
144
+
145
+ ### Indexes
146
+
147
+ An index tracks a collection of looms — upsert entries, subscribe to changes:
148
+
149
+ ```ts
150
+ import { loomRef } from "@deepfates/lync";
151
+ import { createMemoryLoomIndexes } from "@deepfates/lync/indexes/memory";
152
+
153
+ const indexes = createMemoryLoomIndexes();
154
+ const index = await indexes.create({ title: "My looms" });
155
+
156
+ index.subscribe((event) => console.log("index changed:", event.type));
157
+ await index.addLoom(loomRef("loom-1"), { title: "Story" });
158
+
159
+ console.log((await index.entries()).map((entry) => entry.title));
160
+ ```
161
+
162
+ Entries carry a loom reference, optional title/kind/meta, and timestamps.
163
+ `export()`/`import()` round-trip a whole index as a snapshot.
164
+
165
+ ### The loom client
166
+
167
+ One object that pairs looms with an index and resolves
168
+ loom/turn/thread/index references to and from URLs:
169
+
170
+ ```ts
171
+ import { createLyncLooms } from "@deepfates/lync/looms";
172
+ import { createMemoryEventStore } from "@deepfates/lync/memory-log";
173
+ import { createMemoryLoomIndexes } from "@deepfates/lync/indexes/memory";
174
+ import { createLoomClient } from "@deepfates/lync/client";
175
+
176
+ const client = createLoomClient({
177
+ looms: createLyncLooms({ store: createMemoryEventStore(), author: { actor: "you" } }),
178
+ indexes: createMemoryLoomIndexes(),
179
+ });
180
+
181
+ const info = await client.looms.create({ title: "Story" });
182
+ const ref = client.references.loom(info.id);
183
+
184
+ // Round-trip a reference through a shareable URL (?ref=...).
185
+ // In the browser, pass `window.location` instead of a URL object.
186
+ const url = client.references.toUrl(ref, new URL("https://example.com/story"));
187
+ const opened = await client.openReference(client.references.fromUrl(new URL(url)));
188
+ console.log(opened.kind); // "loom" — opened.loom is ready to appendTurn
189
+ ```
190
+
191
+ `@deepfates/lync/client/testing` ships `createTestLoomClient`, a fully in-memory
192
+ client for tests and embedded experiments — deterministic when you pass
193
+ `createId` and `now`.
194
+
195
+ ### Live sync inside an app
196
+
197
+ The sync protocol runs in a browser or Node app with no CLI. Wrap any event
198
+ store in `createSyncedStore`; looms and indexes built over it update live as
199
+ collaborators append, because they already recompute through the store's
200
+ `subscribe`:
201
+
202
+ <!-- example: fragment — needs a live relay and an app render loop; covered by the synced-store tests -->
203
+ ```ts
204
+ import { createMemoryEventStore } from "@deepfates/lync/memory-log";
205
+ import { createLyncLooms } from "@deepfates/lync/looms";
206
+ import { createSyncedStore, createWebSocketTransport } from "@deepfates/lync/synced-store";
207
+
208
+ const transport = createWebSocketTransport("wss://host/lync");
209
+ const store = createSyncedStore(createMemoryEventStore(), transport, {
210
+ onStatus: (s) => console.log("sync:", s.connection, "live:", s.liveRoots),
211
+ });
212
+ const looms = createLyncLooms({ store, author: { actor: "alice" } });
213
+
214
+ const loom = await looms.open(loomId);
215
+ loom.subscribe(() => render(loom)); // fires on local AND remote turns
216
+ await loom.appendTurn(parentId, { text: "typed live" });
217
+ ```
218
+
219
+ Local appends are pushed to the relay; remote lines are ingested through the
220
+ same `union` path and surface reactively. Offline appends queue and flush on
221
+ reconnect; the store re-subscribes automatically. The transport is an
222
+ interface — pass your own for tests or a non-WebSocket carrier. The client
223
+ side uses the platform's built-in WebSocket: no dependency, in the browser or
224
+ in Node.
225
+
226
+ ### Subpath exports
227
+
228
+ - `@deepfates/lync/events` — line parsing, carried-byte export, incremental union
229
+ - `@deepfates/lync/store` — the event-store contract and serialization
230
+ - `@deepfates/lync/memory-log`, `@deepfates/lync/file-log`, `@deepfates/lync/idb-log` — stores
231
+ (`file-log` is node-only; it keeps `node:fs` off the browser path)
232
+ - `@deepfates/lync/views` — branch tree, transcript, memory, leaderboard
233
+ - `@deepfates/lync/looms` — the loom/turn API
234
+ - `@deepfates/lync/references` — loom/turn/thread/index references and URLs
235
+ - `@deepfates/lync/synced-store` — live sync decorator and WebSocket transport
236
+ - `@deepfates/lync/sync-protocol` — the five sync frames, encode/decode
237
+ - `@deepfates/lync/uuid` — zero-dep UUIDv7 for event ids
238
+ - `@deepfates/lync/indexes`, `@deepfates/lync/indexes/entries`,
239
+ `@deepfates/lync/indexes/memory`, `@deepfates/lync/indexes/types` — loom indexes
240
+ - `@deepfates/lync/client`, `@deepfates/lync/client/testing`, `@deepfates/lync/client/types` —
241
+ the loom client
242
+ - `@deepfates/lync/relay` — the sync relay (see [The Relay](#the-relay))
243
+
244
+ ## The Command
245
+
246
+ The package installs a `lync` bin with seven verbs: `init`, `append`,
247
+ `verify`, `merge`, `view`, `serve`, and `sync`.
248
+
249
+ ```bash
250
+ npm install -g @deepfates/lync
251
+ ```
252
+
253
+ ```bash
254
+ lync init story.lync
255
+ printf '%s\n' '{"kind":"notes/text","author":{"actor":"you"},"payload":{"text":"Once..."}}' | lync append story.lync
256
+ lync verify story.lync
257
+ lync view story.lync --as transcript
258
+ printf '%s\n' '{"kind":"notes/text","author":{"actor":"friend"},"payload":{"text":"Then..."}}' | lync append other.lync
259
+ lync merge story.lync other.lync -o merged.lync
260
+ lync view merged.lync --as tree
261
+ ```
262
+
263
+ `append` fills the envelope for you: a UUIDv7 id, the current timestamp, `v`,
264
+ and `parents` default in; anything you supply is kept. `verify` reports what
265
+ every physical line is — accepted, nonconforming, damaged, garbage, or
266
+ conflict variant — and never drops bytes; it exits 0 only when every line is
267
+ accepted. `view` renders `transcript` or `tree`.
268
+
269
+ Any lync file can converge with any other copy through a relay:
270
+
271
+ <!-- example: fragment — needs a live relay pair; the serve/sync path is covered by the relay and cli test suites -->
272
+ ```bash
273
+ lync serve ./rooms --port 8787 # the relay: one append-only file per root
274
+ lync sync story.lync ws://host:8787 # one-shot: push what it lacks, pull what you lack
275
+ lync sync story.lync ws://host:8787 --follow # stay live until Ctrl-C
276
+ ```
277
+
278
+ `lync sync` uses Node's built-in WebSocket — no install beyond the package.
279
+ `lync serve` runs the relay and needs `ws` present (`npm install ws`); see
280
+ below.
281
+
282
+ ## The Relay
283
+
284
+ The relay is deliberately dumb. Events are immutable and merge is union by
285
+ id, so the protocol has no merge logic: five JSON frames (`sub`, `ev`,
286
+ `live`, `presence`, `err`) that move canonical line bytes. The server never
287
+ parses a line beyond extracting its id, stores each root as a plain `.lync`
288
+ file you can read with any lync tool, and echoes accepted events to every
289
+ subscriber — echoes are duplicate no-ops under union. `seq` is a per-root
290
+ arrival counter used as a resume cursor (`<file>.sync.json`), so an offline
291
+ client reconnects exactly where it left off.
292
+
293
+ Running a relay is the one thing that needs a WebSocket server, and Node does
294
+ not ship one — so the relay acquires [`ws`](https://www.npmjs.com/package/ws)
295
+ lazily at the moment you construct it. `@deepfates/lync` declares no dependency on
296
+ `ws` at all: install it yourself next to your server
297
+ (`npm install ws`), and everything else in the package works without it.
298
+ If you bundle a server that runs the relay, mark `ws` as external — the
299
+ acquisition is a dynamic require that bundlers cannot see through.
300
+
301
+ Standalone:
302
+
303
+ <!-- example: daemon — expect "relay on" -->
304
+ ```ts
305
+ import { startLyncServe } from "@deepfates/lync/relay";
306
+
307
+ const server = await startLyncServe({ dir: "./rooms", port: 8787 });
308
+ console.log("relay on", server.port);
309
+
310
+ // Look inside a running relay — read-only, mutates nothing. Each record is
311
+ // { root, generation, seq, subscribers, pendingUnpersisted }, where
312
+ // pendingUnpersisted is the durability lag: lines in memory but not yet on disk.
313
+ for (const room of server.status()) {
314
+ console.log(room.root, "seq", room.seq, "subs", room.subscribers, "lag", room.pendingUnpersisted);
315
+ }
316
+ // later: await server.close();
317
+ ```
318
+
319
+ On an existing HTTP server:
320
+
321
+ <!-- example: fragment — embeds into an existing app server (free variables: app, checkSession) -->
322
+ ```ts
323
+ import { createServer } from "node:http";
324
+ import { attachLyncServer } from "@deepfates/lync/relay";
325
+
326
+ const httpServer = createServer(app);
327
+ const lync = attachLyncServer(httpServer, {
328
+ storageDir: "./rooms",
329
+ path: "/lync", // default
330
+ keepAliveInterval: 30_000, // optional: ping through idle proxies
331
+ maxConnections: 500, // optional
332
+ authenticate: (req) => checkSession(req), // optional, after token check
333
+ });
334
+ httpServer.listen(3000);
335
+ ```
336
+
337
+ For full control, `createLyncRelay` gives you `handleUpgrade` to call from
338
+ your own `upgrade` listener. All three (`createLyncRelay`, `startLyncServe`,
339
+ `attachLyncServer`) expose `status()` — a read-only snapshot of every live
340
+ room's seq, subscriber count, and pending-unpersisted durability lag.
341
+
342
+ Guarantees: same-id-different-bytes is never resolved — both variants are
343
+ kept (a `.conflicts` sidecar) and both sides are told loudly. Persist failures
344
+ are broadcast, never swallowed. A truncated final line after a crash is
345
+ sealed and surfaced as damaged, never eaten. Presence frames are relayed,
346
+ never stored. `--token T` (or `token` in the API) requires
347
+ `Authorization: Bearer T` on every upgrade.
348
+
349
+ The relay is a tool shipped beside the format, not part of it: FORMAT.md
350
+ deliberately excludes sync protocols, and any transport that moves canonical
351
+ line bytes and unions by id converges the same files without this relay.
352
+
353
+ ## Development
354
+
355
+ <!-- example: fragment — repo setup commands that need a checkout, not a scratch dir; scripts/fresh-clone-smoke.sh runs this exact sequence in a temporary clone -->
356
+ ```bash
357
+ pnpm install
358
+ pnpm build
359
+ node bin/lync.js --help
360
+ pnpm test
361
+ pnpm verify
362
+ ```
363
+
364
+ `pnpm verify` runs the path guard, tests, build + typecheck, and executes
365
+ every fenced example in this README against the built package
366
+ (`pnpm check:examples`). README examples are contract: a block runs
367
+ as-written unless an `<!-- example: fragment -->` comment above it declares
368
+ why it can't run alone.
369
+
370
+ For a fresh clone, `pnpm install && pnpm build` is the supported setup
371
+ sequence. After that, `node bin/lync.js --help` prints the CLI help.
372
+ `scripts/fresh-clone-smoke.sh` verifies that sequence in a temporary clone
373
+ and runs the CLI story path: init, append, view, concatenate, merge, and
374
+ verify.
package/bin/lync.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli/bin.js";
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runLyncCli } from "./index.js";
3
+ const code = await runLyncCli(process.argv.slice(2));
4
+ process.exitCode = code;
5
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface LyncCliIO {
2
+ stdout?: Pick<NodeJS.WriteStream, "write">;
3
+ stderr?: Pick<NodeJS.WriteStream, "write">;
4
+ stdin?: NodeJS.ReadStream;
5
+ now?: () => Date;
6
+ randomId?: () => string;
7
+ }
8
+ type ExitCode = 0 | 1 | 2;
9
+ export declare function runLyncCli(argv: string[], io?: LyncCliIO): Promise<ExitCode>;
10
+ export {};
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC;CACzB;AAED,KAAK,QAAQ,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAK1B,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,SAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkCtF"}