@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.
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/bin/lync.js +2 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +404 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/sync.d.ts +42 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +232 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/client/create.d.ts +10 -0
- package/dist/client/create.d.ts.map +1 -0
- package/dist/client/create.js +50 -0
- package/dist/client/create.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/testing.d.ts +14 -0
- package/dist/client/testing.d.ts.map +1 -0
- package/dist/client/testing.js +19 -0
- package/dist/client/testing.js.map +1 -0
- package/dist/client/types.d.ts +49 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +119 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +686 -0
- package/dist/events.js.map +1 -0
- package/dist/file-log.d.ts +25 -0
- package/dist/file-log.d.ts.map +1 -0
- package/dist/file-log.js +97 -0
- package/dist/file-log.js.map +1 -0
- package/dist/idb-log.d.ts +21 -0
- package/dist/idb-log.d.ts.map +1 -0
- package/dist/idb-log.js +114 -0
- package/dist/idb-log.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/indexes/entries.d.ts +6 -0
- package/dist/indexes/entries.d.ts.map +1 -0
- package/dist/indexes/entries.js +7 -0
- package/dist/indexes/entries.js.map +1 -0
- package/dist/indexes/index.d.ts +3 -0
- package/dist/indexes/index.d.ts.map +1 -0
- package/dist/indexes/index.js +3 -0
- package/dist/indexes/index.js.map +1 -0
- package/dist/indexes/memory.d.ts +4 -0
- package/dist/indexes/memory.d.ts.map +1 -0
- package/dist/indexes/memory.js +193 -0
- package/dist/indexes/memory.js.map +1 -0
- package/dist/indexes/types.d.ts +70 -0
- package/dist/indexes/types.d.ts.map +1 -0
- package/dist/indexes/types.js +2 -0
- package/dist/indexes/types.js.map +1 -0
- package/dist/json.d.ts +3 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +15 -0
- package/dist/json.js.map +1 -0
- package/dist/looms.d.ts +25 -0
- package/dist/looms.d.ts.map +1 -0
- package/dist/looms.js +359 -0
- package/dist/looms.js.map +1 -0
- package/dist/memory-log.d.ts +5 -0
- package/dist/memory-log.d.ts.map +1 -0
- package/dist/memory-log.js +7 -0
- package/dist/memory-log.js.map +1 -0
- package/dist/memory.d.ts +4 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +255 -0
- package/dist/memory.js.map +1 -0
- package/dist/profiles/text-story.d.ts +26 -0
- package/dist/profiles/text-story.d.ts.map +1 -0
- package/dist/profiles/text-story.js +55 -0
- package/dist/profiles/text-story.js.map +1 -0
- package/dist/references.d.ts +14 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +109 -0
- package/dist/references.js.map +1 -0
- package/dist/relay/attach.d.ts +24 -0
- package/dist/relay/attach.d.ts.map +1 -0
- package/dist/relay/attach.js +54 -0
- package/dist/relay/attach.js.map +1 -0
- package/dist/relay/index.d.ts +4 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/index.js +4 -0
- package/dist/relay/index.js.map +1 -0
- package/dist/relay/relay.d.ts +80 -0
- package/dist/relay/relay.d.ts.map +1 -0
- package/dist/relay/relay.js +329 -0
- package/dist/relay/relay.js.map +1 -0
- package/dist/relay/serve.d.ts +18 -0
- package/dist/relay/serve.d.ts.map +1 -0
- package/dist/relay/serve.js +37 -0
- package/dist/relay/serve.js.map +1 -0
- package/dist/sha256.d.ts +2 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +84 -0
- package/dist/sha256.js.map +1 -0
- package/dist/store.d.ts +97 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +250 -0
- package/dist/store.js.map +1 -0
- package/dist/sync-protocol.d.ts +80 -0
- package/dist/sync-protocol.d.ts.map +1 -0
- package/dist/sync-protocol.js +135 -0
- package/dist/sync-protocol.js.map +1 -0
- package/dist/synced-store.d.ts +68 -0
- package/dist/synced-store.d.ts.map +1 -0
- package/dist/synced-store.js +340 -0
- package/dist/synced-store.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/uuid.d.ts +9 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +25 -0
- package/dist/uuid.js.map +1 -0
- package/dist/views.d.ts +78 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +192 -0
- package/dist/views.js.map +1 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":""}
|
package/dist/cli/bin.js
ADDED
|
@@ -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"}
|