@amalgm/live 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/PURPOSE.md +103 -0
- package/README.md +45 -0
- package/dist/contracts/binding.d.ts +49 -0
- package/dist/contracts/binding.d.ts.map +1 -0
- package/dist/contracts/binding.js +23 -0
- package/dist/contracts/binding.js.map +1 -0
- package/dist/contracts/content.d.ts +42 -0
- package/dist/contracts/content.d.ts.map +1 -0
- package/dist/contracts/content.js +72 -0
- package/dist/contracts/content.js.map +1 -0
- package/dist/contracts/docs.d.ts +64 -0
- package/dist/contracts/docs.d.ts.map +1 -0
- package/dist/contracts/docs.js +51 -0
- package/dist/contracts/docs.js.map +1 -0
- package/dist/contracts/events.d.ts +45 -0
- package/dist/contracts/events.d.ts.map +1 -0
- package/dist/contracts/events.js +38 -0
- package/dist/contracts/events.js.map +1 -0
- package/dist/contracts/frames/gateway.d.ts +83 -0
- package/dist/contracts/frames/gateway.d.ts.map +1 -0
- package/dist/contracts/frames/gateway.js +35 -0
- package/dist/contracts/frames/gateway.js.map +1 -0
- package/dist/contracts/frames/stream.d.ts +59 -0
- package/dist/contracts/frames/stream.d.ts.map +1 -0
- package/dist/contracts/frames/stream.js +17 -0
- package/dist/contracts/frames/stream.js.map +1 -0
- package/dist/contracts/frames/tunnel.d.ts +78 -0
- package/dist/contracts/frames/tunnel.d.ts.map +1 -0
- package/dist/contracts/frames/tunnel.js +55 -0
- package/dist/contracts/frames/tunnel.js.map +1 -0
- package/dist/contracts/index.d.ts +19 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +19 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/mutation.d.ts +51 -0
- package/dist/contracts/mutation.d.ts.map +1 -0
- package/dist/contracts/mutation.js +55 -0
- package/dist/contracts/mutation.js.map +1 -0
- package/dist/contracts/presence.d.ts +46 -0
- package/dist/contracts/presence.d.ts.map +1 -0
- package/dist/contracts/presence.js +34 -0
- package/dist/contracts/presence.js.map +1 -0
- package/dist/contracts/resources.d.ts +52 -0
- package/dist/contracts/resources.d.ts.map +1 -0
- package/dist/contracts/resources.js +105 -0
- package/dist/contracts/resources.js.map +1 -0
- package/dist/contracts/snapshot.d.ts +24 -0
- package/dist/contracts/snapshot.d.ts.map +1 -0
- package/dist/contracts/snapshot.js +28 -0
- package/dist/contracts/snapshot.js.map +1 -0
- package/dist/contracts/validate.d.ts +32 -0
- package/dist/contracts/validate.d.ts.map +1 -0
- package/dist/contracts/validate.js +25 -0
- package/dist/contracts/validate.js.map +1 -0
- package/dist/detection/adapter-contract.d.ts +134 -0
- package/dist/detection/adapter-contract.d.ts.map +1 -0
- package/dist/detection/adapter-contract.js +162 -0
- package/dist/detection/adapter-contract.js.map +1 -0
- package/dist/detection/classify.d.ts +31 -0
- package/dist/detection/classify.d.ts.map +1 -0
- package/dist/detection/classify.js +28 -0
- package/dist/detection/classify.js.map +1 -0
- package/dist/detection/continuity.d.ts +54 -0
- package/dist/detection/continuity.d.ts.map +1 -0
- package/dist/detection/continuity.js +85 -0
- package/dist/detection/continuity.js.map +1 -0
- package/dist/detection/coverage.d.ts +43 -0
- package/dist/detection/coverage.d.ts.map +1 -0
- package/dist/detection/coverage.js +60 -0
- package/dist/detection/coverage.js.map +1 -0
- package/dist/detection/diff.d.ts +144 -0
- package/dist/detection/diff.d.ts.map +1 -0
- package/dist/detection/diff.js +187 -0
- package/dist/detection/diff.js.map +1 -0
- package/dist/detection/enrollment.d.ts +16 -0
- package/dist/detection/enrollment.d.ts.map +1 -0
- package/dist/detection/enrollment.js +70 -0
- package/dist/detection/enrollment.js.map +1 -0
- package/dist/detection/index.d.ts +29 -0
- package/dist/detection/index.d.ts.map +1 -0
- package/dist/detection/index.js +29 -0
- package/dist/detection/index.js.map +1 -0
- package/dist/detection/refusal.d.ts +33 -0
- package/dist/detection/refusal.d.ts.map +1 -0
- package/dist/detection/refusal.js +40 -0
- package/dist/detection/refusal.js.map +1 -0
- package/dist/entities/bytes.d.ts +18 -0
- package/dist/entities/bytes.d.ts.map +1 -0
- package/dist/entities/bytes.js +61 -0
- package/dist/entities/bytes.js.map +1 -0
- package/dist/entities/cloud.d.ts +82 -0
- package/dist/entities/cloud.d.ts.map +1 -0
- package/dist/entities/cloud.js +136 -0
- package/dist/entities/cloud.js.map +1 -0
- package/dist/entities/index.d.ts +23 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +23 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/moves.d.ts +42 -0
- package/dist/entities/moves.d.ts.map +1 -0
- package/dist/entities/moves.js +82 -0
- package/dist/entities/moves.js.map +1 -0
- package/dist/entities/repo-states.d.ts +121 -0
- package/dist/entities/repo-states.d.ts.map +1 -0
- package/dist/entities/repo-states.js +127 -0
- package/dist/entities/repo-states.js.map +1 -0
- package/dist/entities/rules.d.ts +104 -0
- package/dist/entities/rules.d.ts.map +1 -0
- package/dist/entities/rules.js +200 -0
- package/dist/entities/rules.js.map +1 -0
- package/dist/entities/types.d.ts +94 -0
- package/dist/entities/types.d.ts.map +1 -0
- package/dist/entities/types.js +40 -0
- package/dist/entities/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/machines/arbitration.d.ts +36 -0
- package/dist/machines/arbitration.d.ts.map +1 -0
- package/dist/machines/arbitration.js +37 -0
- package/dist/machines/arbitration.js.map +1 -0
- package/dist/machines/backoff.d.ts +36 -0
- package/dist/machines/backoff.d.ts.map +1 -0
- package/dist/machines/backoff.js +34 -0
- package/dist/machines/backoff.js.map +1 -0
- package/dist/machines/bootstrap.d.ts +65 -0
- package/dist/machines/bootstrap.d.ts.map +1 -0
- package/dist/machines/bootstrap.js +105 -0
- package/dist/machines/bootstrap.js.map +1 -0
- package/dist/machines/chunks.d.ts +51 -0
- package/dist/machines/chunks.d.ts.map +1 -0
- package/dist/machines/chunks.js +67 -0
- package/dist/machines/chunks.js.map +1 -0
- package/dist/machines/index.d.ts +14 -0
- package/dist/machines/index.d.ts.map +1 -0
- package/dist/machines/index.js +14 -0
- package/dist/machines/index.js.map +1 -0
- package/dist/machines/journal.d.ts +63 -0
- package/dist/machines/journal.d.ts.map +1 -0
- package/dist/machines/journal.js +71 -0
- package/dist/machines/journal.js.map +1 -0
- package/dist/machines/ordering.d.ts +53 -0
- package/dist/machines/ordering.d.ts.map +1 -0
- package/dist/machines/ordering.js +87 -0
- package/dist/machines/ordering.js.map +1 -0
- package/dist/machines/presence.d.ts +26 -0
- package/dist/machines/presence.d.ts.map +1 -0
- package/dist/machines/presence.js +49 -0
- package/dist/machines/presence.js.map +1 -0
- package/dist/merge/index.d.ts +9 -0
- package/dist/merge/index.d.ts.map +1 -0
- package/dist/merge/index.js +9 -0
- package/dist/merge/index.js.map +1 -0
- package/dist/merge/merge3.d.ts +39 -0
- package/dist/merge/merge3.d.ts.map +1 -0
- package/dist/merge/merge3.js +188 -0
- package/dist/merge/merge3.js.map +1 -0
- package/package.json +48 -0
package/PURPOSE.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @amalgm/live
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`@amalgm/live` is the portable realtime SDK for the Amalgm platform: one
|
|
6
|
+
library that owns every **wire contract** and every **delivery state machine**
|
|
7
|
+
for synchronizing entities, documents, and presence between a machine and the
|
|
8
|
+
cloud authority. A CLI, desktop app, gateway, service, or third-party
|
|
9
|
+
JavaScript host consumes it without inheriting a filesystem watcher, a SQLite
|
|
10
|
+
database, or a user-interface dependency.
|
|
11
|
+
|
|
12
|
+
The public developer model is `live.bind(surface, channel)` and is specified
|
|
13
|
+
in `docs/realtime-sdk.md`. The shipped wire behavior this package encodes is
|
|
14
|
+
`docs/local-live-protocol.md`. The journal-first write discipline is
|
|
15
|
+
`docs/cloud-mutation-discipline.md`. This repository is where those documents
|
|
16
|
+
become executable and testable; the engine repo keeps read-only copies until
|
|
17
|
+
its cutover.
|
|
18
|
+
|
|
19
|
+
## Axioms
|
|
20
|
+
|
|
21
|
+
1. **The SDK owns portable live contracts, not a machine.** Entity-record
|
|
22
|
+
shapes, resource identifiers, content manifests, checksums, frame shapes,
|
|
23
|
+
mutation envelopes, and ordering rules belong here; paths, inodes,
|
|
24
|
+
watchers, services, and local database files do not.
|
|
25
|
+
2. **A host supplies its own effects.** The SDK receives transport, durable
|
|
26
|
+
journal, content-store, clock, and cryptography adapters. It never reaches
|
|
27
|
+
into Node globals, user homes, or a product-specific HTTP server to find
|
|
28
|
+
them. Nothing in `src/` may import `node:fs`, `node:net`, or spawn a
|
|
29
|
+
process.
|
|
30
|
+
3. **There is one definition of every wire rule.** Client, runtime, and
|
|
31
|
+
gateway must share the same versioned schemas and integrity checks. A
|
|
32
|
+
second copy of a protocol or manifest validator is a bug, not a
|
|
33
|
+
compatibility layer.
|
|
34
|
+
4. **Protocol evolution is additive only.** Every peer treats missing fields
|
|
35
|
+
and unknown frame types as "the other end is older" and keeps the legacy
|
|
36
|
+
behavior. A change that breaks a deployed v1 peer is rejected in review.
|
|
37
|
+
5. **One edit, one mutation identity.** A surface edit creates exactly one
|
|
38
|
+
mutation id, stable across every retry, deduplicated by
|
|
39
|
+
`(resourceId, mutationId)` at every door. Transport packing never merges
|
|
40
|
+
mutation identities.
|
|
41
|
+
6. **Journal first, materialize second.** A durable mutation is journaled
|
|
42
|
+
before local state changes, and that same journal row is the cloud outbox
|
|
43
|
+
record. There is no second outgoing table and no contract-specific retry
|
|
44
|
+
loop.
|
|
45
|
+
7. **Lifecycle state is honest.** A socket being open is never `live`.
|
|
46
|
+
`live` means snapshot applied and ordered delivery confirmed; degraded and
|
|
47
|
+
reconnecting states are reported as themselves.
|
|
48
|
+
8. **Never a silent winner.** Any merge path that durably discards text
|
|
49
|
+
preserves the losing side and says so on the event that carried the
|
|
50
|
+
decision.
|
|
51
|
+
9. **The current runtime remains the single production source until moved.**
|
|
52
|
+
This package extracts contracts and machines with their tests; it does not
|
|
53
|
+
create a parallel execution path. The runtime and gateway adopt it module
|
|
54
|
+
by module, deleting their duplicated copy in the same change.
|
|
55
|
+
10. **Amalgm's own surfaces use this SDK.** A private shortcut in the Amalgm
|
|
56
|
+
UI or runtime means the SDK is incomplete — fix the SDK, not the caller.
|
|
57
|
+
|
|
58
|
+
## Five surfaces, one behavior
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
src/ the SDK — the behavior. Contracts + machines + adapter ports.
|
|
62
|
+
surfaces/api REST router factory a host mounts (diagnose/validate routes).
|
|
63
|
+
surfaces/cli `amalgm-live` — inspect frames, derive ids, simulate merges.
|
|
64
|
+
surfaces/mcp MCP server exposing the same actions to agents.
|
|
65
|
+
skills/live the skill: teaches an agent this protocol via the CLI/MCP.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Every surface is a thin adapter over `src/`. A surface that implements
|
|
69
|
+
behavior instead of delegating to the SDK is a bug (axiom 3).
|
|
70
|
+
|
|
71
|
+
## Layout
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/contracts/ value types + validators: events, snapshots, resource ids,
|
|
75
|
+
mutation envelopes, content manifests, frames (SSE /
|
|
76
|
+
gateway socket / tunnel), doc-write merge outcomes,
|
|
77
|
+
presence.
|
|
78
|
+
src/machines/ transport-neutral state machines: sequence ordering + gap
|
|
79
|
+
recovery, bootstrap barrier, mutation journal, presence
|
|
80
|
+
TTL, slot arbitration, chunk reassembly, backoff.
|
|
81
|
+
src/entities/ the entity model's rule surface: the six-field record,
|
|
82
|
+
derived versions, lifecycle laws, move planning, the
|
|
83
|
+
cloud and repo-state codecs — behind injected ports.
|
|
84
|
+
src/detection/ the Detect/Watch pure cores: the adapter contract, the
|
|
85
|
+
three-channel compare (diff) core, coverage, continuity
|
|
86
|
+
verdicts, classification, refusals, enrollment policy.
|
|
87
|
+
src/adapters/ the ports a host injects: transport, journal store,
|
|
88
|
+
content store, clock, crypto, logger.
|
|
89
|
+
tests/ unit tests per module, in the engine wall's style: each
|
|
90
|
+
exam states a law; mutants convict.
|
|
91
|
+
tests/parity/ golden-vector harness: the same inputs run against this
|
|
92
|
+
package and the shipped runtime implementation must produce
|
|
93
|
+
identical outputs.
|
|
94
|
+
tests/conformance/ the 10-rule suite from cloud-mutation-discipline.md,
|
|
95
|
+
written against the public SDK API only.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Status
|
|
99
|
+
|
|
100
|
+
Extraction stage 1–2 (contracts and machines, per
|
|
101
|
+
`docs/extraction-boundary.md`). The runtime and gateway do not consume this
|
|
102
|
+
package yet; parity tests pin behavior until they do. Until both hosts
|
|
103
|
+
consume it, the engine runtime remains the single production source.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# amalgm-live
|
|
2
|
+
|
|
3
|
+
The portable realtime SDK for the Amalgm platform.
|
|
4
|
+
|
|
5
|
+
One library owns every wire contract and every delivery state machine for
|
|
6
|
+
synchronizing entities, documents, and presence between a machine and the
|
|
7
|
+
cloud authority — behind adapters the host injects. No filesystem, no
|
|
8
|
+
SQLite, no UI dependency.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const binding = live.bind(surface, channel)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Start with [PURPOSE.md](PURPOSE.md) — purpose, axioms, and layout — and
|
|
15
|
+
[ROADMAP.md](ROADMAP.md) for what moves here next. The
|
|
16
|
+
normative specifications live in [docs/](docs/):
|
|
17
|
+
|
|
18
|
+
| document | owns |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| [realtime-sdk.md](docs/realtime-sdk.md) | the public `bind(surface, channel)` model |
|
|
21
|
+
| [local-live-protocol.md](docs/local-live-protocol.md) | the shipped local wire protocol (v1/v2) |
|
|
22
|
+
| [cloud-mutation-discipline.md](docs/cloud-mutation-discipline.md) | the journal-first write discipline and conformance rules |
|
|
23
|
+
| [shared-realtime-wave-3.md](docs/shared-realtime-wave-3.md) | the multi-user cloud-authority design |
|
|
24
|
+
| [extraction-boundary.md](docs/extraction-boundary.md) | what moves here from the engine runtime, and in what order |
|
|
25
|
+
|
|
26
|
+
## Surfaces
|
|
27
|
+
|
|
28
|
+
| surface | entry | what it is |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| SDK | `src/` → `@amalgm/live` | the behavior: contracts, machines, adapter ports |
|
|
31
|
+
| API | `surfaces/api/` | REST router factory a host mounts |
|
|
32
|
+
| CLI | `surfaces/cli/` | `amalgm-live` — inspect, derive, validate, simulate |
|
|
33
|
+
| MCP | `surfaces/mcp/` | the same actions, exposed to agents |
|
|
34
|
+
| Skill | `skills/live/` | teaches an agent this protocol via the CLI/MCP |
|
|
35
|
+
|
|
36
|
+
Every surface delegates to `src/`. One behavior, five doors.
|
|
37
|
+
|
|
38
|
+
## Develop
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install
|
|
42
|
+
npm test # build + unit tests
|
|
43
|
+
npm run test:parity # golden vectors vs the shipped engine runtime
|
|
44
|
+
npm run test:conformance
|
|
45
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The binding — `live.bind(surface, channel)` (realtime-sdk.md).
|
|
3
|
+
*
|
|
4
|
+
* The surface knows what the user is looking at and what counts as one
|
|
5
|
+
* edit. The channel identifies the shared thing and its delivery class.
|
|
6
|
+
* The binding moves changes both ways and exposes honest lifecycle state:
|
|
7
|
+
* a socket being open is never `live` (axiom 7).
|
|
8
|
+
*/
|
|
9
|
+
/** Delivery classes (realtime-sdk.md §6). Media setup shares the binding; media packets never ride the durable log. */
|
|
10
|
+
export type DeliveryClass = 'durable' | 'ephemeral' | 'media';
|
|
11
|
+
/** A channel is a stable reference, not a socket. Authority and permissions come from the authenticated resource record — never from client fields. */
|
|
12
|
+
export interface LiveChannelRef {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** Must match the surface contract, e.g. "text-yjs@1". */
|
|
15
|
+
readonly contract: string;
|
|
16
|
+
readonly delivery: DeliveryClass;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Connection lifecycle vocabulary (realtime-sdk.md §7). `live` means the
|
|
20
|
+
* snapshot is applied AND ordered delivery is confirmed; `degraded` means
|
|
21
|
+
* delivery works through a slower fallback (e.g. gateway poll).
|
|
22
|
+
*/
|
|
23
|
+
export declare const BINDING_STATES: readonly ["connecting", "hydrating", "live", "degraded", "reconnecting", "blocked", "revoked"];
|
|
24
|
+
export type BindingState = (typeof BINDING_STATES)[number];
|
|
25
|
+
export interface ApplyContext {
|
|
26
|
+
/** True while a snapshot install is in progress; surfaces must not echo. */
|
|
27
|
+
readonly hydrating: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface SnapshotContext {
|
|
30
|
+
/** Why the snapshot is being installed: first hydration or gap recovery. */
|
|
31
|
+
readonly reason: 'hydration' | 'recovery';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A surface is an adapter over a real interactive UI — not a React
|
|
35
|
+
* component type, not a transport client (realtime-sdk.md §1). It defines
|
|
36
|
+
* the edit boundary, emits exactly one operation per edit, applies remote
|
|
37
|
+
* operations in place, and never echoes a remotely applied operation back
|
|
38
|
+
* as a new local edit.
|
|
39
|
+
*/
|
|
40
|
+
export interface LiveSurface<Snapshot, Operation, Presence = never> {
|
|
41
|
+
readonly contract: string;
|
|
42
|
+
observeEdits(emit: (operation: Operation) => void): () => void;
|
|
43
|
+
applyRemote(operation: Operation, context: ApplyContext): void;
|
|
44
|
+
/** Initial hydration or recovery only — never the normal response to a remote edit. */
|
|
45
|
+
applySnapshot(snapshot: Snapshot, context: SnapshotContext): void;
|
|
46
|
+
observePresence?(emit: (presence: Presence | null) => void): () => void;
|
|
47
|
+
applyPresence?(participantId: string, presence: Presence | null): void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/contracts/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,uHAAuH;AACvH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC;AAE9D,uJAAuJ;AACvJ,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,gGAQjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,GAAG,KAAK;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,YAAY,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC/D,WAAW,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/D,uFAAuF;IACvF,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAElE,eAAe,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,aAAa,CAAC,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;CACxE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The binding — `live.bind(surface, channel)` (realtime-sdk.md).
|
|
3
|
+
*
|
|
4
|
+
* The surface knows what the user is looking at and what counts as one
|
|
5
|
+
* edit. The channel identifies the shared thing and its delivery class.
|
|
6
|
+
* The binding moves changes both ways and exposes honest lifecycle state:
|
|
7
|
+
* a socket being open is never `live` (axiom 7).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Connection lifecycle vocabulary (realtime-sdk.md §7). `live` means the
|
|
11
|
+
* snapshot is applied AND ordered delivery is confirmed; `degraded` means
|
|
12
|
+
* delivery works through a slower fallback (e.g. gateway poll).
|
|
13
|
+
*/
|
|
14
|
+
export const BINDING_STATES = [
|
|
15
|
+
'connecting',
|
|
16
|
+
'hydrating',
|
|
17
|
+
'live',
|
|
18
|
+
'degraded',
|
|
19
|
+
'reconnecting',
|
|
20
|
+
'blocked',
|
|
21
|
+
'revoked',
|
|
22
|
+
];
|
|
23
|
+
//# sourceMappingURL=binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.js","sourceRoot":"","sources":["../../src/contracts/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,YAAY;IACZ,WAAW;IACX,MAAM;IACN,UAAU;IACV,cAAc;IACd,SAAS;IACT,SAAS;CACD,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content manifests — the wire shape of immutable entity bytes.
|
|
3
|
+
*
|
|
4
|
+
* A file's content travels as bounded, content-addressed chunks named by a
|
|
5
|
+
* manifest (engine registration/entity-content.js, contract
|
|
6
|
+
* `amalgm-entity-content@1`). Every writer derives the same manifest from
|
|
7
|
+
* the same bytes, so manifests are idempotent facts, never negotiations.
|
|
8
|
+
*
|
|
9
|
+
* The chunk rule is `bytes <= CHUNK_BYTES`, never an exact size — older,
|
|
10
|
+
* smaller chunks remain valid forever. Capture, storage, and upload are
|
|
11
|
+
* host effects; this file owns the shape and its laws.
|
|
12
|
+
*/
|
|
13
|
+
import { type Checked } from './validate.js';
|
|
14
|
+
export declare const CONTENT_CONTRACT = "amalgm-entity-content@1";
|
|
15
|
+
/** One chunk is one WAN round trip; the gateway accepts parts up to this same bound. */
|
|
16
|
+
export declare const CHUNK_BYTES: number;
|
|
17
|
+
export interface ContentChunk {
|
|
18
|
+
readonly sha256: string;
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ContentManifest {
|
|
22
|
+
readonly contract: typeof CONTENT_CONTRACT;
|
|
23
|
+
readonly contentHash: string;
|
|
24
|
+
readonly bytes: number;
|
|
25
|
+
readonly chunks: readonly ContentChunk[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate and normalize a manifest. The laws, in conviction order:
|
|
29
|
+
* well-formed hashes; every chunk within the bound; chunk bytes sum to the
|
|
30
|
+
* total; empty content has no chunks. Error phrases match the engine's —
|
|
31
|
+
* they are the ratified refusal text.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkContentManifest(input: unknown, expectedHash?: string | null): Checked<ContentManifest>;
|
|
34
|
+
/**
|
|
35
|
+
* Derive the manifest for already-chunked bytes. The host reads and hashes
|
|
36
|
+
* (sha256 hex of each part and of the whole); this seals the shape so a
|
|
37
|
+
* hand-built manifest cannot drift from the derivation rule.
|
|
38
|
+
*/
|
|
39
|
+
export declare function manifestFromChunks(contentHash: string, chunks: readonly ContentChunk[]): Checked<ContentManifest>;
|
|
40
|
+
/** Split a byte count into the chunk sizes capture will produce: full chunks, then the remainder. Zero bytes → zero chunks. */
|
|
41
|
+
export declare function chunkSizes(totalBytes: number): number[];
|
|
42
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/contracts/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,eAAe,CAAC;AAEnE,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,wFAAwF;AACxF,eAAO,MAAM,WAAW,QAAkB,CAAC;AAI3C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,gBAAgB,CAAC;IAC3C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAkCjH;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,YAAY,EAAE,GAC9B,OAAO,CAAC,eAAe,CAAC,CAK1B;AAED,+HAA+H;AAC/H,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content manifests — the wire shape of immutable entity bytes.
|
|
3
|
+
*
|
|
4
|
+
* A file's content travels as bounded, content-addressed chunks named by a
|
|
5
|
+
* manifest (engine registration/entity-content.js, contract
|
|
6
|
+
* `amalgm-entity-content@1`). Every writer derives the same manifest from
|
|
7
|
+
* the same bytes, so manifests are idempotent facts, never negotiations.
|
|
8
|
+
*
|
|
9
|
+
* The chunk rule is `bytes <= CHUNK_BYTES`, never an exact size — older,
|
|
10
|
+
* smaller chunks remain valid forever. Capture, storage, and upload are
|
|
11
|
+
* host effects; this file owns the shape and its laws.
|
|
12
|
+
*/
|
|
13
|
+
import { pass, fail, isRecord } from './validate.js';
|
|
14
|
+
export const CONTENT_CONTRACT = 'amalgm-entity-content@1';
|
|
15
|
+
/** One chunk is one WAN round trip; the gateway accepts parts up to this same bound. */
|
|
16
|
+
export const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
18
|
+
/**
|
|
19
|
+
* Validate and normalize a manifest. The laws, in conviction order:
|
|
20
|
+
* well-formed hashes; every chunk within the bound; chunk bytes sum to the
|
|
21
|
+
* total; empty content has no chunks. Error phrases match the engine's —
|
|
22
|
+
* they are the ratified refusal text.
|
|
23
|
+
*/
|
|
24
|
+
export function checkContentManifest(input, expectedHash = null) {
|
|
25
|
+
if (!isRecord(input) ||
|
|
26
|
+
typeof input.contentHash !== 'string' ||
|
|
27
|
+
!HASH.test(input.contentHash) ||
|
|
28
|
+
(expectedHash !== null && input.contentHash !== expectedHash) ||
|
|
29
|
+
!Number.isSafeInteger(input.bytes) ||
|
|
30
|
+
input.bytes < 0 ||
|
|
31
|
+
!Array.isArray(input.chunks) ||
|
|
32
|
+
input.chunks.some((chunk) => !isRecord(chunk) ||
|
|
33
|
+
!HASH.test(String(chunk.sha256 ?? '')) ||
|
|
34
|
+
!Number.isSafeInteger(chunk.bytes) ||
|
|
35
|
+
chunk.bytes < 1 ||
|
|
36
|
+
chunk.bytes > CHUNK_BYTES)) {
|
|
37
|
+
return fail('entity content manifest is invalid');
|
|
38
|
+
}
|
|
39
|
+
const bytes = input.bytes;
|
|
40
|
+
const chunks = input.chunks;
|
|
41
|
+
if (chunks.reduce((total, chunk) => total + chunk.bytes, 0) !== bytes) {
|
|
42
|
+
return fail('entity content manifest byte count is invalid');
|
|
43
|
+
}
|
|
44
|
+
if (bytes === 0 && chunks.length !== 0) {
|
|
45
|
+
return fail('empty entity content must have no chunks');
|
|
46
|
+
}
|
|
47
|
+
return pass({
|
|
48
|
+
contract: CONTENT_CONTRACT,
|
|
49
|
+
contentHash: input.contentHash,
|
|
50
|
+
bytes,
|
|
51
|
+
chunks: chunks.map((chunk) => ({ sha256: chunk.sha256, bytes: chunk.bytes })),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derive the manifest for already-chunked bytes. The host reads and hashes
|
|
56
|
+
* (sha256 hex of each part and of the whole); this seals the shape so a
|
|
57
|
+
* hand-built manifest cannot drift from the derivation rule.
|
|
58
|
+
*/
|
|
59
|
+
export function manifestFromChunks(contentHash, chunks) {
|
|
60
|
+
return checkContentManifest({ contract: CONTENT_CONTRACT, contentHash, bytes: chunks.reduce((t, c) => t + c.bytes, 0), chunks }, contentHash);
|
|
61
|
+
}
|
|
62
|
+
/** Split a byte count into the chunk sizes capture will produce: full chunks, then the remainder. Zero bytes → zero chunks. */
|
|
63
|
+
export function chunkSizes(totalBytes) {
|
|
64
|
+
if (!Number.isSafeInteger(totalBytes) || totalBytes < 0)
|
|
65
|
+
return [];
|
|
66
|
+
const sizes = [];
|
|
67
|
+
for (let remaining = totalBytes; remaining > 0; remaining -= CHUNK_BYTES) {
|
|
68
|
+
sizes.push(Math.min(remaining, CHUNK_BYTES));
|
|
69
|
+
}
|
|
70
|
+
return sizes;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/contracts/content.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned document writes — the contract for whole-text writers.
|
|
3
|
+
*
|
|
4
|
+
* `POST /state/docs/write` (local-live-protocol.md §8) exists so a writer
|
|
5
|
+
* that read a file, thought, and wrote back does not clobber live edits
|
|
6
|
+
* that landed in between. `baseHash` is the lowercase sha256 hex of the
|
|
7
|
+
* EXACT text the writer read; every response's `sha256` chains into the
|
|
8
|
+
* next write.
|
|
9
|
+
*
|
|
10
|
+
* The governing invariant — never a silent winner: any path that durably
|
|
11
|
+
* discards text preserves it in a sidecar first and surfaces the decision
|
|
12
|
+
* on the doc event.
|
|
13
|
+
*/
|
|
14
|
+
import { type Checked } from './validate.js';
|
|
15
|
+
/** Text over this limit is refused with 413. */
|
|
16
|
+
export declare const MAX_DOC_BYTES: number;
|
|
17
|
+
/** Bases resolve against a ring of recent disk-sync states, per doc. */
|
|
18
|
+
export declare const RETENTION_RING: {
|
|
19
|
+
readonly states: 8;
|
|
20
|
+
readonly maxTotalBytes: number;
|
|
21
|
+
readonly maxAgeMs: number;
|
|
22
|
+
};
|
|
23
|
+
export interface DocWriteRequest {
|
|
24
|
+
readonly path: string;
|
|
25
|
+
/** Full new file contents — this is a whole-text contract. */
|
|
26
|
+
readonly text: string;
|
|
27
|
+
/** sha256 hex of the exact text the writer read. Prefer this over baseSeq. */
|
|
28
|
+
readonly baseHash?: string;
|
|
29
|
+
/** Exact-match lookup hint, consulted only when no baseHash was sent. */
|
|
30
|
+
readonly baseSeq?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every outcome states what happens to the losing text in the same breath.
|
|
34
|
+
*
|
|
35
|
+
* | outcome | when | losing text |
|
|
36
|
+
* |----------------|----------------------------------------|------------------------|
|
|
37
|
+
* | `noop` | incoming === live | none exists |
|
|
38
|
+
* | `clean` | base === live | none exists |
|
|
39
|
+
* | `merged` | base in retention, no overlapping hunks| both sides land |
|
|
40
|
+
* | `conflict` | overlapping hunks | full incoming → sidecar|
|
|
41
|
+
* | `base-unknown` | base outside the retention window | live text → sidecar |
|
|
42
|
+
* | `overwrite` | no base sent | replaced text → sidecar unless nothing was deleted |
|
|
43
|
+
*/
|
|
44
|
+
export declare const MERGE_OUTCOMES: readonly ["noop", "clean", "merged", "conflict", "base-unknown", "overwrite"];
|
|
45
|
+
export type MergeOutcome = (typeof MERGE_OUTCOMES)[number];
|
|
46
|
+
export declare const CONFLICT_REASONS: readonly ["overlapping-hunks", "unversioned-overwrite", "base-unknown"];
|
|
47
|
+
export type ConflictReason = (typeof CONFLICT_REASONS)[number];
|
|
48
|
+
/** Travels on the write response and on the doc event (`patch.conflict`, source `doc:write`). */
|
|
49
|
+
export interface DocConflict {
|
|
50
|
+
readonly reason: ConflictReason;
|
|
51
|
+
readonly sidecarPath: string;
|
|
52
|
+
readonly hunks?: number;
|
|
53
|
+
readonly at: number;
|
|
54
|
+
}
|
|
55
|
+
export interface DocWriteResponse {
|
|
56
|
+
readonly merge: MergeOutcome;
|
|
57
|
+
readonly seq: number;
|
|
58
|
+
/** Hash of the resulting text — hold it as the next write's baseHash. */
|
|
59
|
+
readonly sha256: string;
|
|
60
|
+
readonly conflict: DocConflict | null;
|
|
61
|
+
}
|
|
62
|
+
export declare const isSha256Hex: (value: unknown) => value is string;
|
|
63
|
+
export declare function checkDocWriteRequest(input: unknown): Checked<DocWriteRequest>;
|
|
64
|
+
//# sourceMappingURL=docs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/contracts/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAsD,MAAM,eAAe,CAAC;AAEjG,gDAAgD;AAChD,eAAO,MAAM,aAAa,QAAkB,CAAC;AAE7C,wEAAwE;AACxE,eAAO,MAAM,cAAc;;;;CAAgF,CAAC;AAE5G,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,+EAAgF,CAAC;AAC5G,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AACxG,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;CACvC;AAID,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACK,CAAC;AAE5D,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAa7E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versioned document writes — the contract for whole-text writers.
|
|
3
|
+
*
|
|
4
|
+
* `POST /state/docs/write` (local-live-protocol.md §8) exists so a writer
|
|
5
|
+
* that read a file, thought, and wrote back does not clobber live edits
|
|
6
|
+
* that landed in between. `baseHash` is the lowercase sha256 hex of the
|
|
7
|
+
* EXACT text the writer read; every response's `sha256` chains into the
|
|
8
|
+
* next write.
|
|
9
|
+
*
|
|
10
|
+
* The governing invariant — never a silent winner: any path that durably
|
|
11
|
+
* discards text preserves it in a sidecar first and surfaces the decision
|
|
12
|
+
* on the doc event.
|
|
13
|
+
*/
|
|
14
|
+
import { pass, failShape, isRecord, isNonEmptyString, isSeq } from './validate.js';
|
|
15
|
+
/** Text over this limit is refused with 413. */
|
|
16
|
+
export const MAX_DOC_BYTES = 2 * 1024 * 1024;
|
|
17
|
+
/** Bases resolve against a ring of recent disk-sync states, per doc. */
|
|
18
|
+
export const RETENTION_RING = { states: 8, maxTotalBytes: 4 * 1024 * 1024, maxAgeMs: 10 * 60_000 };
|
|
19
|
+
/**
|
|
20
|
+
* Every outcome states what happens to the losing text in the same breath.
|
|
21
|
+
*
|
|
22
|
+
* | outcome | when | losing text |
|
|
23
|
+
* |----------------|----------------------------------------|------------------------|
|
|
24
|
+
* | `noop` | incoming === live | none exists |
|
|
25
|
+
* | `clean` | base === live | none exists |
|
|
26
|
+
* | `merged` | base in retention, no overlapping hunks| both sides land |
|
|
27
|
+
* | `conflict` | overlapping hunks | full incoming → sidecar|
|
|
28
|
+
* | `base-unknown` | base outside the retention window | live text → sidecar |
|
|
29
|
+
* | `overwrite` | no base sent | replaced text → sidecar unless nothing was deleted |
|
|
30
|
+
*/
|
|
31
|
+
export const MERGE_OUTCOMES = ['noop', 'clean', 'merged', 'conflict', 'base-unknown', 'overwrite'];
|
|
32
|
+
export const CONFLICT_REASONS = ['overlapping-hunks', 'unversioned-overwrite', 'base-unknown'];
|
|
33
|
+
const REQUEST_EXAMPLE = '{ path: "notes.md", text: "full new contents", baseHash: "9f86d0…" }';
|
|
34
|
+
export const isSha256Hex = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
|
35
|
+
export function checkDocWriteRequest(input) {
|
|
36
|
+
if (!isRecord(input))
|
|
37
|
+
return failShape(REQUEST_EXAMPLE, `got ${input === null ? 'null' : typeof input}`);
|
|
38
|
+
if (!isNonEmptyString(input.path)) {
|
|
39
|
+
return failShape(REQUEST_EXAMPLE, `path was ${JSON.stringify(input.path)}, not a non-empty string`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof input.text !== 'string')
|
|
42
|
+
return failShape(REQUEST_EXAMPLE, 'text was missing — this is a whole-text write');
|
|
43
|
+
if (input.baseHash !== undefined && !isSha256Hex(input.baseHash)) {
|
|
44
|
+
return failShape(REQUEST_EXAMPLE, `baseHash was ${JSON.stringify(input.baseHash)}, not lowercase sha256 hex`);
|
|
45
|
+
}
|
|
46
|
+
if (input.baseSeq !== undefined && !isSeq(input.baseSeq)) {
|
|
47
|
+
return failShape(REQUEST_EXAMPLE, `baseSeq was ${JSON.stringify(input.baseSeq)}, not a non-negative integer`);
|
|
48
|
+
}
|
|
49
|
+
return pass(input);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=docs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/contracts/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAgB,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEjG,gDAAgD;AAChD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE7C,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,EAAW,CAAC;AAY5G;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,CAAU,CAAC;AAG5G,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,mBAAmB,EAAE,uBAAuB,EAAE,cAAc,CAAU,CAAC;AAmBxG,MAAM,eAAe,GAAG,sEAAsE,CAAC;AAE/F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAc,EAAmB,EAAE,CAC7D,OAAO,KAAK,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAE5D,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC,eAAe,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;IACzG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAC,eAAe,EAAE,YAAY,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACtG,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,eAAe,EAAE,+CAA+C,CAAC,CAAC;IACvH,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjE,OAAO,SAAS,CAAC,eAAe,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,4BAA4B,CAAC,CAAC;IAChH,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,OAAO,SAAS,CAAC,eAAe,EAAE,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAChH,CAAC;IACD,OAAO,IAAI,CAAC,KAAmC,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State events — the unit of change on the local wire.
|
|
3
|
+
*
|
|
4
|
+
* Every change is an event (local-live-protocol.md §1). Tables and disk are
|
|
5
|
+
* truth; the event log is a bounded (~15 min) delivery buffer, so a client
|
|
6
|
+
* that falls off the buffer re-snapshots rather than asking for history.
|
|
7
|
+
*
|
|
8
|
+
* `seq` is a strictly increasing integer per runtime. The two optional
|
|
9
|
+
* Wave 3 fields (`mutationId`, `sharedResourceId`) are additive: legacy
|
|
10
|
+
* rows omit both keys exactly as before, and validation here accepts both
|
|
11
|
+
* generations (axiom: additive-only evolution).
|
|
12
|
+
*/
|
|
13
|
+
import { type Checked } from './validate.js';
|
|
14
|
+
import type { ResourceName } from './resources.js';
|
|
15
|
+
/** The shipped wire operations. `move`/`delta` (realtime-sdk.md §3) ride as contract-specific patches until a wire rev adds them. */
|
|
16
|
+
export declare const WIRE_OPS: readonly ["insert", "update", "delete", "replace", "invalidate"];
|
|
17
|
+
export type WireOp = (typeof WIRE_OPS)[number];
|
|
18
|
+
export interface StateEvent {
|
|
19
|
+
readonly seq: number;
|
|
20
|
+
readonly ts: number;
|
|
21
|
+
readonly resource: ResourceName;
|
|
22
|
+
readonly op: WireOp;
|
|
23
|
+
/** Addressed item within the resource, when the op targets one. */
|
|
24
|
+
readonly id?: string;
|
|
25
|
+
readonly value?: unknown;
|
|
26
|
+
readonly patch?: unknown;
|
|
27
|
+
/** Provenance, e.g. 'doc:write'. */
|
|
28
|
+
readonly source?: string;
|
|
29
|
+
/** Echo of the client-supplied mutation id, for optimistic reconciliation. */
|
|
30
|
+
readonly clientMutationId?: string;
|
|
31
|
+
readonly version?: number;
|
|
32
|
+
/** Wave 3, additive: durable mutation identity. */
|
|
33
|
+
readonly mutationId?: string;
|
|
34
|
+
/** Wave 3, additive: the cloud id of the shared resource this event belongs to. */
|
|
35
|
+
readonly sharedResourceId?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare const isWireOp: (value: unknown) => value is WireOp;
|
|
38
|
+
/**
|
|
39
|
+
* Validate one wire event. Unknown extra keys are preserved untouched —
|
|
40
|
+
* a newer peer may be speaking an additive extension we do not know yet.
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkStateEvent(input: unknown): Checked<StateEvent>;
|
|
43
|
+
/** Events apply in seq order; equal or older seq means already applied (client obligation, protocol §6). */
|
|
44
|
+
export declare const isNewerThan: (event: StateEvent, appliedSeq: number) => boolean;
|
|
45
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/contracts/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAsD,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,qIAAqI;AACrI,eAAO,MAAM,QAAQ,kEAAmE,CAAC;AACzF,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAID,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MAC2B,CAAC;AAE/E;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CASnE;AAED,4GAA4G;AAC5G,eAAO,MAAM,WAAW,GAAI,OAAO,UAAU,EAAE,YAAY,MAAM,KAAG,OAAiC,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State events — the unit of change on the local wire.
|
|
3
|
+
*
|
|
4
|
+
* Every change is an event (local-live-protocol.md §1). Tables and disk are
|
|
5
|
+
* truth; the event log is a bounded (~15 min) delivery buffer, so a client
|
|
6
|
+
* that falls off the buffer re-snapshots rather than asking for history.
|
|
7
|
+
*
|
|
8
|
+
* `seq` is a strictly increasing integer per runtime. The two optional
|
|
9
|
+
* Wave 3 fields (`mutationId`, `sharedResourceId`) are additive: legacy
|
|
10
|
+
* rows omit both keys exactly as before, and validation here accepts both
|
|
11
|
+
* generations (axiom: additive-only evolution).
|
|
12
|
+
*/
|
|
13
|
+
import { pass, failShape, isRecord, isNonEmptyString, isSeq } from './validate.js';
|
|
14
|
+
/** The shipped wire operations. `move`/`delta` (realtime-sdk.md §3) ride as contract-specific patches until a wire rev adds them. */
|
|
15
|
+
export const WIRE_OPS = ['insert', 'update', 'delete', 'replace', 'invalidate'];
|
|
16
|
+
const EXAMPLE = '{ seq: 42, ts: 1719000000000, resource: "tasks", op: "update", id: "t1", patch: {...} }';
|
|
17
|
+
export const isWireOp = (value) => typeof value === 'string' && WIRE_OPS.includes(value);
|
|
18
|
+
/**
|
|
19
|
+
* Validate one wire event. Unknown extra keys are preserved untouched —
|
|
20
|
+
* a newer peer may be speaking an additive extension we do not know yet.
|
|
21
|
+
*/
|
|
22
|
+
export function checkStateEvent(input) {
|
|
23
|
+
if (!isRecord(input))
|
|
24
|
+
return failShape(EXAMPLE, `got ${input === null ? 'null' : typeof input}`);
|
|
25
|
+
if (!isSeq(input.seq))
|
|
26
|
+
return failShape(EXAMPLE, `seq was ${JSON.stringify(input.seq)}, not a non-negative integer`);
|
|
27
|
+
if (typeof input.ts !== 'number')
|
|
28
|
+
return failShape(EXAMPLE, `ts was ${JSON.stringify(input.ts)}, not a number`);
|
|
29
|
+
if (!isNonEmptyString(input.resource))
|
|
30
|
+
return failShape(EXAMPLE, `resource was ${JSON.stringify(input.resource)}, not a name`);
|
|
31
|
+
if (!isWireOp(input.op)) {
|
|
32
|
+
return failShape(EXAMPLE, `op was ${JSON.stringify(input.op)}, not one of ${WIRE_OPS.join('|')}`);
|
|
33
|
+
}
|
|
34
|
+
return pass(input);
|
|
35
|
+
}
|
|
36
|
+
/** Events apply in seq order; equal or older seq means already applied (client obligation, protocol §6). */
|
|
37
|
+
export const isNewerThan = (event, appliedSeq) => event.seq > appliedSeq;
|
|
38
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/contracts/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAgB,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGjG,qIAAqI;AACrI,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAuBzF,MAAM,OAAO,GAAG,yFAAyF,CAAC;AAE1G,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAmB,EAAE,CAC1D,OAAO,KAAK,KAAK,QAAQ,IAAK,QAA8B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAE/E;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;IACjG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IACrH,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;IAChH,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC,OAAO,EAAE,gBAAgB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC/H,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,IAAI,CAAC,KAA8B,CAAC,CAAC;AAC9C,CAAC;AAED,4GAA4G;AAC5G,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAiB,EAAE,UAAkB,EAAW,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,UAAU,CAAC"}
|