@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.
Files changed (159) hide show
  1. package/PURPOSE.md +103 -0
  2. package/README.md +45 -0
  3. package/dist/contracts/binding.d.ts +49 -0
  4. package/dist/contracts/binding.d.ts.map +1 -0
  5. package/dist/contracts/binding.js +23 -0
  6. package/dist/contracts/binding.js.map +1 -0
  7. package/dist/contracts/content.d.ts +42 -0
  8. package/dist/contracts/content.d.ts.map +1 -0
  9. package/dist/contracts/content.js +72 -0
  10. package/dist/contracts/content.js.map +1 -0
  11. package/dist/contracts/docs.d.ts +64 -0
  12. package/dist/contracts/docs.d.ts.map +1 -0
  13. package/dist/contracts/docs.js +51 -0
  14. package/dist/contracts/docs.js.map +1 -0
  15. package/dist/contracts/events.d.ts +45 -0
  16. package/dist/contracts/events.d.ts.map +1 -0
  17. package/dist/contracts/events.js +38 -0
  18. package/dist/contracts/events.js.map +1 -0
  19. package/dist/contracts/frames/gateway.d.ts +83 -0
  20. package/dist/contracts/frames/gateway.d.ts.map +1 -0
  21. package/dist/contracts/frames/gateway.js +35 -0
  22. package/dist/contracts/frames/gateway.js.map +1 -0
  23. package/dist/contracts/frames/stream.d.ts +59 -0
  24. package/dist/contracts/frames/stream.d.ts.map +1 -0
  25. package/dist/contracts/frames/stream.js +17 -0
  26. package/dist/contracts/frames/stream.js.map +1 -0
  27. package/dist/contracts/frames/tunnel.d.ts +78 -0
  28. package/dist/contracts/frames/tunnel.d.ts.map +1 -0
  29. package/dist/contracts/frames/tunnel.js +55 -0
  30. package/dist/contracts/frames/tunnel.js.map +1 -0
  31. package/dist/contracts/index.d.ts +19 -0
  32. package/dist/contracts/index.d.ts.map +1 -0
  33. package/dist/contracts/index.js +19 -0
  34. package/dist/contracts/index.js.map +1 -0
  35. package/dist/contracts/mutation.d.ts +51 -0
  36. package/dist/contracts/mutation.d.ts.map +1 -0
  37. package/dist/contracts/mutation.js +55 -0
  38. package/dist/contracts/mutation.js.map +1 -0
  39. package/dist/contracts/presence.d.ts +46 -0
  40. package/dist/contracts/presence.d.ts.map +1 -0
  41. package/dist/contracts/presence.js +34 -0
  42. package/dist/contracts/presence.js.map +1 -0
  43. package/dist/contracts/resources.d.ts +52 -0
  44. package/dist/contracts/resources.d.ts.map +1 -0
  45. package/dist/contracts/resources.js +105 -0
  46. package/dist/contracts/resources.js.map +1 -0
  47. package/dist/contracts/snapshot.d.ts +24 -0
  48. package/dist/contracts/snapshot.d.ts.map +1 -0
  49. package/dist/contracts/snapshot.js +28 -0
  50. package/dist/contracts/snapshot.js.map +1 -0
  51. package/dist/contracts/validate.d.ts +32 -0
  52. package/dist/contracts/validate.d.ts.map +1 -0
  53. package/dist/contracts/validate.js +25 -0
  54. package/dist/contracts/validate.js.map +1 -0
  55. package/dist/detection/adapter-contract.d.ts +134 -0
  56. package/dist/detection/adapter-contract.d.ts.map +1 -0
  57. package/dist/detection/adapter-contract.js +162 -0
  58. package/dist/detection/adapter-contract.js.map +1 -0
  59. package/dist/detection/classify.d.ts +31 -0
  60. package/dist/detection/classify.d.ts.map +1 -0
  61. package/dist/detection/classify.js +28 -0
  62. package/dist/detection/classify.js.map +1 -0
  63. package/dist/detection/continuity.d.ts +54 -0
  64. package/dist/detection/continuity.d.ts.map +1 -0
  65. package/dist/detection/continuity.js +85 -0
  66. package/dist/detection/continuity.js.map +1 -0
  67. package/dist/detection/coverage.d.ts +43 -0
  68. package/dist/detection/coverage.d.ts.map +1 -0
  69. package/dist/detection/coverage.js +60 -0
  70. package/dist/detection/coverage.js.map +1 -0
  71. package/dist/detection/diff.d.ts +144 -0
  72. package/dist/detection/diff.d.ts.map +1 -0
  73. package/dist/detection/diff.js +187 -0
  74. package/dist/detection/diff.js.map +1 -0
  75. package/dist/detection/enrollment.d.ts +16 -0
  76. package/dist/detection/enrollment.d.ts.map +1 -0
  77. package/dist/detection/enrollment.js +70 -0
  78. package/dist/detection/enrollment.js.map +1 -0
  79. package/dist/detection/index.d.ts +29 -0
  80. package/dist/detection/index.d.ts.map +1 -0
  81. package/dist/detection/index.js +29 -0
  82. package/dist/detection/index.js.map +1 -0
  83. package/dist/detection/refusal.d.ts +33 -0
  84. package/dist/detection/refusal.d.ts.map +1 -0
  85. package/dist/detection/refusal.js +40 -0
  86. package/dist/detection/refusal.js.map +1 -0
  87. package/dist/entities/bytes.d.ts +18 -0
  88. package/dist/entities/bytes.d.ts.map +1 -0
  89. package/dist/entities/bytes.js +61 -0
  90. package/dist/entities/bytes.js.map +1 -0
  91. package/dist/entities/cloud.d.ts +82 -0
  92. package/dist/entities/cloud.d.ts.map +1 -0
  93. package/dist/entities/cloud.js +136 -0
  94. package/dist/entities/cloud.js.map +1 -0
  95. package/dist/entities/index.d.ts +23 -0
  96. package/dist/entities/index.d.ts.map +1 -0
  97. package/dist/entities/index.js +23 -0
  98. package/dist/entities/index.js.map +1 -0
  99. package/dist/entities/moves.d.ts +42 -0
  100. package/dist/entities/moves.d.ts.map +1 -0
  101. package/dist/entities/moves.js +82 -0
  102. package/dist/entities/moves.js.map +1 -0
  103. package/dist/entities/repo-states.d.ts +121 -0
  104. package/dist/entities/repo-states.d.ts.map +1 -0
  105. package/dist/entities/repo-states.js +127 -0
  106. package/dist/entities/repo-states.js.map +1 -0
  107. package/dist/entities/rules.d.ts +104 -0
  108. package/dist/entities/rules.d.ts.map +1 -0
  109. package/dist/entities/rules.js +200 -0
  110. package/dist/entities/rules.js.map +1 -0
  111. package/dist/entities/types.d.ts +94 -0
  112. package/dist/entities/types.d.ts.map +1 -0
  113. package/dist/entities/types.js +40 -0
  114. package/dist/entities/types.js.map +1 -0
  115. package/dist/index.d.ts +17 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +17 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/machines/arbitration.d.ts +36 -0
  120. package/dist/machines/arbitration.d.ts.map +1 -0
  121. package/dist/machines/arbitration.js +37 -0
  122. package/dist/machines/arbitration.js.map +1 -0
  123. package/dist/machines/backoff.d.ts +36 -0
  124. package/dist/machines/backoff.d.ts.map +1 -0
  125. package/dist/machines/backoff.js +34 -0
  126. package/dist/machines/backoff.js.map +1 -0
  127. package/dist/machines/bootstrap.d.ts +65 -0
  128. package/dist/machines/bootstrap.d.ts.map +1 -0
  129. package/dist/machines/bootstrap.js +105 -0
  130. package/dist/machines/bootstrap.js.map +1 -0
  131. package/dist/machines/chunks.d.ts +51 -0
  132. package/dist/machines/chunks.d.ts.map +1 -0
  133. package/dist/machines/chunks.js +67 -0
  134. package/dist/machines/chunks.js.map +1 -0
  135. package/dist/machines/index.d.ts +14 -0
  136. package/dist/machines/index.d.ts.map +1 -0
  137. package/dist/machines/index.js +14 -0
  138. package/dist/machines/index.js.map +1 -0
  139. package/dist/machines/journal.d.ts +63 -0
  140. package/dist/machines/journal.d.ts.map +1 -0
  141. package/dist/machines/journal.js +71 -0
  142. package/dist/machines/journal.js.map +1 -0
  143. package/dist/machines/ordering.d.ts +53 -0
  144. package/dist/machines/ordering.d.ts.map +1 -0
  145. package/dist/machines/ordering.js +87 -0
  146. package/dist/machines/ordering.js.map +1 -0
  147. package/dist/machines/presence.d.ts +26 -0
  148. package/dist/machines/presence.d.ts.map +1 -0
  149. package/dist/machines/presence.js +49 -0
  150. package/dist/machines/presence.js.map +1 -0
  151. package/dist/merge/index.d.ts +9 -0
  152. package/dist/merge/index.d.ts.map +1 -0
  153. package/dist/merge/index.js +9 -0
  154. package/dist/merge/index.js.map +1 -0
  155. package/dist/merge/merge3.d.ts +39 -0
  156. package/dist/merge/merge3.d.ts.map +1 -0
  157. package/dist/merge/merge3.js +188 -0
  158. package/dist/merge/merge3.js.map +1 -0
  159. package/package.json +48 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The bootstrap machine — how a client reaches `live` truthfully.
3
+ *
4
+ * Ordered-bootstrap mode (local-live-protocol.md §3): the snapshot is the
5
+ * first data frame and everything after it is ordered on the same channel,
6
+ * so the machine simply installs and follows. Legacy mode: the snapshot
7
+ * travels out-of-band, so the machine holds events while the snapshot body
8
+ * is in flight and replays only those past its seq — the full-snapshot
9
+ * barrier (§6).
10
+ *
11
+ * Feature detection (§3): a bootstrap request against a legacy engine
12
+ * yields no `snapshot` frame. The client falls back to the legacy path
13
+ * when the first data frame is a `state` event, a `ping` arrives, or the
14
+ * timeout passes — whichever comes first.
15
+ */
16
+ import { BOOTSTRAP_SNAPSHOT_TIMEOUT_MS } from '../contracts/frames/stream.js';
17
+ /** One instance per bootstrap-mode connection attempt; discard after it resolves. */
18
+ export class BootstrapMachine {
19
+ #held = [];
20
+ #resolved = false;
21
+ timeoutMs = BOOTSTRAP_SNAPSHOT_TIMEOUT_MS;
22
+ /** Feed each stream frame until a command other than `none` resolves the machine. */
23
+ accept(frame) {
24
+ if (this.#resolved)
25
+ return { command: 'none' };
26
+ switch (frame.type) {
27
+ case 'snapshot': {
28
+ this.#resolved = true;
29
+ const snapshot = { seq: frame.seq, stable: frame.stable !== false, resources: frame.resources };
30
+ const replay = this.#held.filter((event) => event.seq > frame.seq);
31
+ this.#held = [];
32
+ return { command: 'install', snapshot, replay };
33
+ }
34
+ case 'state': {
35
+ if (this.#held.length === 0) {
36
+ // First data frame is an event, not a snapshot: legacy peer.
37
+ this.#resolved = true;
38
+ return { command: 'fallback-legacy', held: [frame.event], reason: 'state-before-snapshot' };
39
+ }
40
+ this.#held.push(frame.event);
41
+ return { command: 'none' };
42
+ }
43
+ case 'ping': {
44
+ this.#resolved = true;
45
+ const held = this.#held;
46
+ this.#held = [];
47
+ return { command: 'fallback-legacy', held, reason: 'ping-before-snapshot' };
48
+ }
49
+ case 'error': {
50
+ this.#resolved = true;
51
+ this.#held = [];
52
+ return frame.status === undefined ? { command: 'fallback-error' } : { command: 'fallback-error', status: frame.status };
53
+ }
54
+ case 'reset':
55
+ case 'presence':
56
+ return { command: 'none' };
57
+ }
58
+ }
59
+ /** The host's timer fired without a snapshot frame. */
60
+ expire() {
61
+ if (this.#resolved)
62
+ return { command: 'none' };
63
+ this.#resolved = true;
64
+ const held = this.#held;
65
+ this.#held = [];
66
+ return { command: 'fallback-legacy', held, reason: 'timeout' };
67
+ }
68
+ }
69
+ /**
70
+ * The legacy full-snapshot barrier (§6): hold events while a full snapshot
71
+ * body is in flight; on arrival, replay only those past its seq. Partial
72
+ * snapshots (resources added on a live connection) never pass through
73
+ * here — they hold per-resource and never bump the connection seq.
74
+ */
75
+ export class SnapshotBarrier {
76
+ #held = [];
77
+ #inFlight = false;
78
+ get inFlight() {
79
+ return this.#inFlight;
80
+ }
81
+ begin() {
82
+ this.#inFlight = true;
83
+ this.#held = [];
84
+ }
85
+ /** Returns true when the event was held (a snapshot body is in flight). */
86
+ hold(event) {
87
+ if (!this.#inFlight)
88
+ return false;
89
+ this.#held.push(event);
90
+ return true;
91
+ }
92
+ /** The snapshot body landed: install it, then apply the returned events in order. */
93
+ complete(snapshot) {
94
+ this.#inFlight = false;
95
+ const replay = this.#held.filter((event) => event.seq > snapshot.seq).sort((a, b) => a.seq - b.seq);
96
+ this.#held = [];
97
+ return replay;
98
+ }
99
+ /** The snapshot fetch failed: release nothing; the caller re-syncs. */
100
+ abandon() {
101
+ this.#inFlight = false;
102
+ this.#held = [];
103
+ }
104
+ }
105
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../../src/machines/bootstrap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Chunked response reassembly — the receiving half of §5's
3
+ * res_start / res_chunk / res_end train.
4
+ *
5
+ * Chunks reassemble by concatenating in index order; `res_end.chunks` must
6
+ * match the count received, else the whole response is treated as a 502
7
+ * for that request. Control frames interleaving between chunks is the
8
+ * feature, so the assembler tolerates any arrival interleaving across
9
+ * requests — but never within one (indexes must arrive 0,1,2,…).
10
+ */
11
+ import type { ChunkedResponseFrame } from '../contracts/frames/tunnel.js';
12
+ export interface AssembledResponse {
13
+ readonly status: number;
14
+ readonly headers: Readonly<Record<string, string>>;
15
+ /** Base64 chunks in index order; the host decodes with its own codec. */
16
+ readonly bodyChunksB64: readonly string[];
17
+ }
18
+ export type AssemblyResult = {
19
+ readonly done: false;
20
+ } | {
21
+ readonly done: true;
22
+ readonly response: AssembledResponse;
23
+ }
24
+ /** Protocol violation for this req_id — treat as a 502 and drop the accumulator. */
25
+ | {
26
+ readonly done: true;
27
+ readonly error: string;
28
+ };
29
+ /**
30
+ * One assembler per tunnel connection. The host must call `abandon` when a
31
+ * request times out — the accumulator is memory, and §5 requires cleanup
32
+ * on the request timeout (AMALGM_TUNNEL_REQUEST_TIMEOUT_MS).
33
+ */
34
+ export declare class ChunkAssembler {
35
+ #private;
36
+ /** In-flight accumulator count, exposed for diagnostics (§5 getTunnelDebugStats). */
37
+ get pending(): number;
38
+ accept(frame: ChunkedResponseFrame): AssemblyResult;
39
+ /** Drop a timed-out request's accumulator. Returns whether one existed. */
40
+ abandon(reqId: string): boolean;
41
+ }
42
+ /**
43
+ * Sender-side chunk plan for a raw body size: how many chunks a v2 peer
44
+ * will receive. The budget check happens BEFORE materializing any frame
45
+ * (§5): an over-budget response is dropped as a 503 without ever building
46
+ * its chunks — hence the plan is sizes only, never buffers.
47
+ */
48
+ export declare function planChunks(bodyBytes: number, chunkMaxBytes: number): {
49
+ chunks: number;
50
+ };
51
+ //# sourceMappingURL=chunks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.d.ts","sourceRoot":"","sources":["../../src/machines/chunks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,yEAAyE;IACzE,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE;AAC/D,oFAAoF;GAClF;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAQpD;;;;GAIG;AACH,qBAAa,cAAc;;IAGzB,qFAAqF;IACrF,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,cAAc;IA4BnD,2EAA2E;IAC3E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;CAGhC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAGvF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Chunked response reassembly — the receiving half of §5's
3
+ * res_start / res_chunk / res_end train.
4
+ *
5
+ * Chunks reassemble by concatenating in index order; `res_end.chunks` must
6
+ * match the count received, else the whole response is treated as a 502
7
+ * for that request. Control frames interleaving between chunks is the
8
+ * feature, so the assembler tolerates any arrival interleaving across
9
+ * requests — but never within one (indexes must arrive 0,1,2,…).
10
+ */
11
+ /**
12
+ * One assembler per tunnel connection. The host must call `abandon` when a
13
+ * request times out — the accumulator is memory, and §5 requires cleanup
14
+ * on the request timeout (AMALGM_TUNNEL_REQUEST_TIMEOUT_MS).
15
+ */
16
+ export class ChunkAssembler {
17
+ #pending = new Map();
18
+ /** In-flight accumulator count, exposed for diagnostics (§5 getTunnelDebugStats). */
19
+ get pending() {
20
+ return this.#pending.size;
21
+ }
22
+ accept(frame) {
23
+ switch (frame.type) {
24
+ case 'res_start': {
25
+ this.#pending.set(frame.req_id, { status: frame.status, headers: frame.headers, chunks: [] });
26
+ return { done: false };
27
+ }
28
+ case 'res_chunk': {
29
+ const acc = this.#pending.get(frame.req_id);
30
+ if (!acc)
31
+ return { done: true, error: `res_chunk for unknown req_id ${frame.req_id}` };
32
+ if (frame.index !== acc.chunks.length) {
33
+ this.#pending.delete(frame.req_id);
34
+ return { done: true, error: `res_chunk index ${frame.index} arrived out of order (expected ${acc.chunks.length})` };
35
+ }
36
+ acc.chunks.push(frame.body_b64);
37
+ return { done: false };
38
+ }
39
+ case 'res_end': {
40
+ const acc = this.#pending.get(frame.req_id);
41
+ this.#pending.delete(frame.req_id);
42
+ if (!acc)
43
+ return { done: true, error: `res_end for unknown req_id ${frame.req_id}` };
44
+ if (frame.chunks !== acc.chunks.length) {
45
+ return { done: true, error: `res_end declared ${frame.chunks} chunks but ${acc.chunks.length} arrived` };
46
+ }
47
+ return { done: true, response: { status: acc.status, headers: acc.headers, bodyChunksB64: acc.chunks } };
48
+ }
49
+ }
50
+ }
51
+ /** Drop a timed-out request's accumulator. Returns whether one existed. */
52
+ abandon(reqId) {
53
+ return this.#pending.delete(reqId);
54
+ }
55
+ }
56
+ /**
57
+ * Sender-side chunk plan for a raw body size: how many chunks a v2 peer
58
+ * will receive. The budget check happens BEFORE materializing any frame
59
+ * (§5): an over-budget response is dropped as a 503 without ever building
60
+ * its chunks — hence the plan is sizes only, never buffers.
61
+ */
62
+ export function planChunks(bodyBytes, chunkMaxBytes) {
63
+ if (bodyBytes < 0 || !Number.isSafeInteger(bodyBytes))
64
+ return { chunks: 0 };
65
+ return { chunks: bodyBytes === 0 ? 1 : Math.ceil(bodyBytes / chunkMaxBytes) };
66
+ }
67
+ //# sourceMappingURL=chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunks.js","sourceRoot":"","sources":["../../src/machines/chunks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The machines layer: the protocol's behavior as pure, deterministic state
3
+ * machines. Every machine takes wire shapes in and returns decisions as
4
+ * data — the host performs the fetches, timers, and writes. Nothing in
5
+ * here opens a socket, reads a clock, or touches a disk.
6
+ */
7
+ export * from './ordering.js';
8
+ export * from './bootstrap.js';
9
+ export * from './journal.js';
10
+ export * from './presence.js';
11
+ export * from './arbitration.js';
12
+ export * from './chunks.js';
13
+ export * from './backoff.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/machines/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The machines layer: the protocol's behavior as pure, deterministic state
3
+ * machines. Every machine takes wire shapes in and returns decisions as
4
+ * data — the host performs the fetches, timers, and writes. Nothing in
5
+ * here opens a socket, reads a clock, or touches a disk.
6
+ */
7
+ export * from './ordering.js';
8
+ export * from './bootstrap.js';
9
+ export * from './journal.js';
10
+ export * from './presence.js';
11
+ export * from './arbitration.js';
12
+ export * from './chunks.js';
13
+ export * from './backoff.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/machines/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The mutation journal machine — journal first, materialize second.
3
+ *
4
+ * The hard rule (cloud-mutation-discipline.md): one SDK commit creates one
5
+ * durable mutation identity; the journal row is written BEFORE the local
6
+ * materialization, and that same row is the cloud outbox record. There is
7
+ * no second outgoing table and no contract-specific retry loop.
8
+ *
9
+ * This machine owns the state transitions and their legality. The host
10
+ * supplies the durable store through the `MutationJournalStore` port and
11
+ * performs the actual materialization and network submission; the cloud
12
+ * pump drives `claim → synced/rejected/conflicted`.
13
+ */
14
+ import { type LiveMutation, type MutationState } from '../contracts/mutation.js';
15
+ export interface JournalRow<Operation = unknown> {
16
+ readonly mutation: LiveMutation<Operation>;
17
+ readonly state: MutationState;
18
+ /** Set once the cloud assigns the official per-resource version. */
19
+ readonly officialVersion?: number;
20
+ /** Present on `rejected`/`conflicted`; local work is preserved either way. */
21
+ readonly reason?: string;
22
+ }
23
+ /** The durable store port. Every method is synchronous from the machine's view; a host may back it with SQLite, IndexedDB, or memory. */
24
+ export interface MutationJournalStore<Operation = unknown> {
25
+ read(key: string): JournalRow<Operation> | null;
26
+ write(key: string, row: JournalRow<Operation>): void;
27
+ /** The outbox is a filtered view of this journal — never a second table. */
28
+ pendingShared(): readonly JournalRow<Operation>[];
29
+ }
30
+ export type JournalDecision<Operation = unknown> =
31
+ /** New identity: the row is journaled; the host must now materialize, then call `advance(key, 'saved-local')`. */
32
+ {
33
+ readonly outcome: 'journaled';
34
+ readonly row: JournalRow<Operation>;
35
+ }
36
+ /** Same `(resourceId, mutationId)` seen before: return the existing row — a retry never creates a second edit. */
37
+ | {
38
+ readonly outcome: 'duplicate';
39
+ readonly row: JournalRow<Operation>;
40
+ };
41
+ export declare class MutationJournal<Operation = unknown> {
42
+ private readonly store;
43
+ constructor(store: MutationJournalStore<Operation>);
44
+ /** SDK commit: journal the mutation before anything else happens. Idempotent by mutation identity. */
45
+ commit(mutation: LiveMutation<Operation>): JournalDecision<Operation>;
46
+ /**
47
+ * Advance a mutation's state along the discipline's order. An illegal
48
+ * transition throws — it is a programming error in the host, not a wire
49
+ * condition, and hiding it would corrupt the journal's meaning.
50
+ */
51
+ advance(key: string, to: MutationState, detail?: {
52
+ officialVersion?: number;
53
+ reason?: string;
54
+ }): JournalRow<Operation>;
55
+ /**
56
+ * Crash recovery: an unfinished materialization is resumed from the
57
+ * journal already on disk — rows still `saving-local` need their
58
+ * materialization re-applied (idempotently), rows `saved-local` and
59
+ * later resume from the pump.
60
+ */
61
+ unfinishedMaterializations(): readonly JournalRow<Operation>[];
62
+ }
63
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.d.ts","sourceRoot":"","sources":["../../src/machines/journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAe,MAAM,0BAA0B,CAAC;AAE9F,MAAM,WAAW,UAAU,CAAC,SAAS,GAAG,OAAO;IAC7C,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,yIAAyI;AACzI,MAAM,WAAW,oBAAoB,CAAC,SAAS,GAAG,OAAO;IACvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IACrD,4EAA4E;IAC5E,aAAa,IAAI,SAAS,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;CACnD;AAWD,MAAM,MAAM,eAAe,CAAC,SAAS,GAAG,OAAO;AAC7C,kHAAkH;AAChH;IAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,SAAS,CAAC,CAAA;CAAE;AACxE,kHAAkH;GAChH;IAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,SAAS,CAAC,CAAA;CAAE,CAAC;AAE3E,qBAAa,eAAe,CAAC,SAAS,GAAG,OAAO;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,oBAAoB,CAAC,SAAS,CAAC;IAEnE,sGAAsG;IACtG,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,GAAG,eAAe,CAAC,SAAS,CAAC;IASrE;;;;OAIG;IACH,OAAO,CACL,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,aAAa,EACjB,MAAM,GAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GACzD,UAAU,CAAC,SAAS,CAAC;IAqBxB;;;;;OAKG;IACH,0BAA0B,IAAI,SAAS,UAAU,CAAC,SAAS,CAAC,EAAE;CAG/D"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The mutation journal machine — journal first, materialize second.
3
+ *
4
+ * The hard rule (cloud-mutation-discipline.md): one SDK commit creates one
5
+ * durable mutation identity; the journal row is written BEFORE the local
6
+ * materialization, and that same row is the cloud outbox record. There is
7
+ * no second outgoing table and no contract-specific retry loop.
8
+ *
9
+ * This machine owns the state transitions and their legality. The host
10
+ * supplies the durable store through the `MutationJournalStore` port and
11
+ * performs the actual materialization and network submission; the cloud
12
+ * pump drives `claim → synced/rejected/conflicted`.
13
+ */
14
+ import { mutationKey } from '../contracts/mutation.js';
15
+ const LEGAL = {
16
+ 'saving-local': ['saved-local'],
17
+ 'saved-local': ['syncing'],
18
+ syncing: ['synced', 'rejected', 'conflicted', 'saved-local'], // saved-local: a lease expired; the pump re-claims
19
+ synced: [],
20
+ rejected: [],
21
+ conflicted: [],
22
+ };
23
+ export class MutationJournal {
24
+ store;
25
+ constructor(store) {
26
+ this.store = store;
27
+ }
28
+ /** SDK commit: journal the mutation before anything else happens. Idempotent by mutation identity. */
29
+ commit(mutation) {
30
+ const key = mutationKey(mutation);
31
+ const existing = this.store.read(key);
32
+ if (existing)
33
+ return { outcome: 'duplicate', row: existing };
34
+ const row = { mutation, state: 'saving-local' };
35
+ this.store.write(key, row);
36
+ return { outcome: 'journaled', row };
37
+ }
38
+ /**
39
+ * Advance a mutation's state along the discipline's order. An illegal
40
+ * transition throws — it is a programming error in the host, not a wire
41
+ * condition, and hiding it would corrupt the journal's meaning.
42
+ */
43
+ advance(key, to, detail = {}) {
44
+ const row = this.store.read(key);
45
+ if (!row) {
46
+ throw new Error(`no journal row for "${key}" — commit() the mutation before advancing it`);
47
+ }
48
+ if (!LEGAL[row.state].includes(to)) {
49
+ throw new Error(`illegal journal transition "${row.state}" → "${to}" for "${key}" — ` +
50
+ `the order is saving-local → saved-local → syncing → synced|rejected|conflicted`);
51
+ }
52
+ const next = {
53
+ mutation: row.mutation,
54
+ state: to,
55
+ ...(detail.officialVersion !== undefined ? { officialVersion: detail.officialVersion } : {}),
56
+ ...(detail.reason !== undefined ? { reason: detail.reason } : {}),
57
+ };
58
+ this.store.write(key, next);
59
+ return next;
60
+ }
61
+ /**
62
+ * Crash recovery: an unfinished materialization is resumed from the
63
+ * journal already on disk — rows still `saving-local` need their
64
+ * materialization re-applied (idempotently), rows `saved-local` and
65
+ * later resume from the pump.
66
+ */
67
+ unfinishedMaterializations() {
68
+ return this.store.pendingShared().filter((row) => row.state === 'saving-local');
69
+ }
70
+ }
71
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/machines/journal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAyC,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAmB9F,MAAM,KAAK,GAA8D;IACvE,cAAc,EAAE,CAAC,aAAa,CAAC;IAC/B,aAAa,EAAE,CAAC,SAAS,CAAC;IAC1B,OAAO,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,CAAC,EAAE,mDAAmD;IACjH,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;CACf,CAAC;AAQF,MAAM,OAAO,eAAe;IACG;IAA7B,YAA6B,KAAsC;QAAtC,UAAK,GAAL,KAAK,CAAiC;IAAG,CAAC;IAEvE,sGAAsG;IACtG,MAAM,CAAC,QAAiC;QACtC,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;QAC7D,MAAM,GAAG,GAA0B,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC;QACvE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC3B,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,OAAO,CACL,GAAW,EACX,EAAiB,EACjB,SAAwD,EAAE;QAE1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,uBAAuB,GAAG,+CAA+C,CAAC,CAAC;QAC7F,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CACb,+BAA+B,GAAG,CAAC,KAAK,QAAQ,EAAE,UAAU,GAAG,MAAM;gBACnE,gFAAgF,CACnF,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAA0B;YAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,KAAK,EAAE,EAAE;YACT,GAAG,CAAC,MAAM,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5F,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACH,0BAA0B;QACxB,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC;IAClF,CAAC;CACF"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The sequence ledger — ordered, gap-free application of state events.
3
+ *
4
+ * Client obligations (local-live-protocol.md §6): never re-apply an event
5
+ * at or below the applied seq; on a gap, run exactly one backfill at a
6
+ * time (`GET /state/events?after=`); on backfill failure or a `reset`
7
+ * frame, converge on a full re-sync. Every decision is returned as data —
8
+ * the host performs the fetches.
9
+ */
10
+ import type { StateEvent } from '../contracts/events.js';
11
+ export type LedgerDecision =
12
+ /** In order: apply to local state, advance the ledger. */
13
+ {
14
+ readonly action: 'apply';
15
+ readonly event: StateEvent;
16
+ }
17
+ /** At or below the applied seq: drop silently (duplicate delivery is normal). */
18
+ | {
19
+ readonly action: 'skip-duplicate';
20
+ readonly event: StateEvent;
21
+ }
22
+ /** A gap opened. Run one backfill after `after`; the event is buffered and will replay once the gap closes. */
23
+ | {
24
+ readonly action: 'backfill';
25
+ readonly after: number;
26
+ readonly buffered: StateEvent;
27
+ }
28
+ /** A backfill is already in flight: buffer only (single-flight obligation). */
29
+ | {
30
+ readonly action: 'buffer';
31
+ readonly buffered: StateEvent;
32
+ };
33
+ export declare class SequenceLedger {
34
+ #private;
35
+ constructor(appliedSeq: number);
36
+ get appliedSeq(): number;
37
+ get backfilling(): boolean;
38
+ /** Feed one live event; act on the returned decision. */
39
+ accept(event: StateEvent): LedgerDecision;
40
+ /**
41
+ * Deliver the backfill body. Returns the events to apply, in order,
42
+ * merged with everything buffered while the backfill was in flight.
43
+ * Backfilled and buffered ranges may overlap; the ledger dedupes by seq.
44
+ */
45
+ completeBackfill(events: readonly StateEvent[]): StateEvent[];
46
+ /** Whether `completeBackfill` left the ledger still gapped — the caller must re-sync. */
47
+ needsResync(nextBufferedSeq?: number): boolean;
48
+ /** Backfill failed, or a `reset` frame arrived: drop everything and let the caller re-snapshot. */
49
+ abandonToResync(): void;
50
+ /** A full snapshot installed at `seq` supersedes anything older. */
51
+ resumeFrom(seq: number): void;
52
+ }
53
+ //# sourceMappingURL=ordering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ordering.d.ts","sourceRoot":"","sources":["../../src/machines/ordering.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,MAAM,cAAc;AACxB,0DAA0D;AACxD;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAAE;AAC1D,iFAAiF;GAC/E;IAAE,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAAE;AACnE,+GAA+G;GAC7G;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;CAAE;AACxF,+EAA+E;GAC7E;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAA;CAAE,CAAC;AAEjE,qBAAa,cAAc;;gBAKb,UAAU,EAAE,MAAM;IAI9B,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,yDAAyD;IACzD,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc;IAezC;;;;OAIG;IACH,gBAAgB,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE;IAmB7D,yFAAyF;IACzF,WAAW,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO;IAI9C,mGAAmG;IACnG,eAAe,IAAI,IAAI;IAKvB,oEAAoE;IACpE,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAY9B"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The sequence ledger — ordered, gap-free application of state events.
3
+ *
4
+ * Client obligations (local-live-protocol.md §6): never re-apply an event
5
+ * at or below the applied seq; on a gap, run exactly one backfill at a
6
+ * time (`GET /state/events?after=`); on backfill failure or a `reset`
7
+ * frame, converge on a full re-sync. Every decision is returned as data —
8
+ * the host performs the fetches.
9
+ */
10
+ export class SequenceLedger {
11
+ #appliedSeq;
12
+ #backfilling = false;
13
+ #buffer = [];
14
+ constructor(appliedSeq) {
15
+ this.#appliedSeq = appliedSeq;
16
+ }
17
+ get appliedSeq() {
18
+ return this.#appliedSeq;
19
+ }
20
+ get backfilling() {
21
+ return this.#backfilling;
22
+ }
23
+ /** Feed one live event; act on the returned decision. */
24
+ accept(event) {
25
+ if (event.seq <= this.#appliedSeq)
26
+ return { action: 'skip-duplicate', event };
27
+ if (this.#backfilling) {
28
+ this.#insertBuffered(event);
29
+ return { action: 'buffer', buffered: event };
30
+ }
31
+ if (event.seq === this.#appliedSeq + 1) {
32
+ this.#appliedSeq = event.seq;
33
+ return { action: 'apply', event };
34
+ }
35
+ this.#backfilling = true;
36
+ this.#insertBuffered(event);
37
+ return { action: 'backfill', after: this.#appliedSeq, buffered: event };
38
+ }
39
+ /**
40
+ * Deliver the backfill body. Returns the events to apply, in order,
41
+ * merged with everything buffered while the backfill was in flight.
42
+ * Backfilled and buffered ranges may overlap; the ledger dedupes by seq.
43
+ */
44
+ completeBackfill(events) {
45
+ this.#backfilling = false;
46
+ const merged = [...events, ...this.#buffer].sort((a, b) => a.seq - b.seq);
47
+ this.#buffer = [];
48
+ const toApply = [];
49
+ for (const event of merged) {
50
+ if (event.seq <= this.#appliedSeq)
51
+ continue;
52
+ if (event.seq !== this.#appliedSeq + 1) {
53
+ // The backfill itself was gappy — the buffer cannot repair it.
54
+ // Converge on the recovery rule: full re-sync.
55
+ this.#buffer = [];
56
+ return toApply;
57
+ }
58
+ this.#appliedSeq = event.seq;
59
+ toApply.push(event);
60
+ }
61
+ return toApply;
62
+ }
63
+ /** Whether `completeBackfill` left the ledger still gapped — the caller must re-sync. */
64
+ needsResync(nextBufferedSeq) {
65
+ return nextBufferedSeq !== undefined && nextBufferedSeq > this.#appliedSeq + 1;
66
+ }
67
+ /** Backfill failed, or a `reset` frame arrived: drop everything and let the caller re-snapshot. */
68
+ abandonToResync() {
69
+ this.#backfilling = false;
70
+ this.#buffer = [];
71
+ }
72
+ /** A full snapshot installed at `seq` supersedes anything older. */
73
+ resumeFrom(seq) {
74
+ this.#appliedSeq = seq;
75
+ this.#backfilling = false;
76
+ this.#buffer = this.#buffer.filter((event) => event.seq > seq);
77
+ }
78
+ #insertBuffered(event) {
79
+ // Keep the buffer sorted and deduped; delivery may repeat frames.
80
+ const at = this.#buffer.findIndex((held) => held.seq >= event.seq);
81
+ if (at === -1)
82
+ this.#buffer.push(event);
83
+ else if (this.#buffer[at].seq !== event.seq)
84
+ this.#buffer.splice(at, 0, event);
85
+ }
86
+ }
87
+ //# sourceMappingURL=ordering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ordering.js","sourceRoot":"","sources":["../../src/machines/ordering.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Presence receiver — arrival-clock TTL over replace-not-merge frames.
3
+ *
4
+ * A presence frame carries the FULL peer list for one resource; receivers
5
+ * replace their view and treat absence as the default that publishing
6
+ * re-earns (local-live-protocol.md §2). The server already expires stale
7
+ * publishers, but receivers must apply their OWN arrival-clock TTL — a
8
+ * frozen stream must not preserve ghosts.
9
+ */
10
+ import { type PresenceFrame, type PresencePeer } from '../contracts/presence.js';
11
+ export interface SeenPeer extends PresencePeer {
12
+ /** Receiver's clock when the peer last arrived — TTL runs on this, never on sender timestamps. */
13
+ readonly arrivedAtMs: number;
14
+ }
15
+ export declare class PresenceView {
16
+ #private;
17
+ private readonly ttlMs;
18
+ constructor(ttlMs?: number);
19
+ /** Replace one resource's peer view. `nowMs` is the receiver's clock (host-supplied — the SDK holds no clock). */
20
+ accept(frame: PresenceFrame, nowMs: number): void;
21
+ /** Live peers for a resource, with expired entries dropped as of `nowMs`. */
22
+ peers(resource: string, nowMs: number): readonly SeenPeer[];
23
+ /** Delivery was lost: presence is not replayed after a gap, so the view must not outlive its stream. */
24
+ clear(): void;
25
+ }
26
+ //# sourceMappingURL=presence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/machines/presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAEhG,MAAM,WAAW,QAAS,SAAQ,YAAY;IAC5C,kGAAkG;IAClG,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,qBAAa,YAAY;;IAGX,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,GAAE,MAAsB;IAE1D,kHAAkH;IAClH,MAAM,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IASjD,6EAA6E;IAC7E,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,QAAQ,EAAE;IAY3D,wGAAwG;IACxG,KAAK,IAAI,IAAI;CAGd"}