@adobe/data-sync 0.9.90

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.
@@ -0,0 +1,156 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ /**
3
+ * Creates a sync server that accepts client proposals, assigns canonical
4
+ * commit timestamps, and broadcasts committed envelopes to all connected
5
+ * clients.
6
+ *
7
+ * The server is intentionally minimal:
8
+ * - It does **not** maintain a database — it is a pure ordering service.
9
+ * - Proposals are accepted in FIFO order (first-come-first-served).
10
+ * - No authentication or validation is performed here; layer those on top via
11
+ * a middleware pattern (intercept `propose` messages before passing to
12
+ * `SyncServer`).
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const server = createSyncServer();
17
+ * // When a WebSocket opens:
18
+ * const disconnect = server.connect(webSocketServerTransport);
19
+ * // When the WebSocket closes:
20
+ * disconnect();
21
+ * ```
22
+ */
23
+ const generateId = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
24
+ ? crypto.randomUUID()
25
+ : Math.random().toString(36).slice(2) + Date.now().toString(36);
26
+ export const createSyncServer = (options = {}) => {
27
+ const { livenessTimeoutMs = 25_000, logger } = options;
28
+ const log = logger ?? (() => undefined);
29
+ const sessionId = generateId();
30
+ log(`session ${sessionId} starting`);
31
+ const clients = new Set();
32
+ const unsubscribers = new Map();
33
+ const lastInboundAt = new Map();
34
+ // Ordered log of every committed envelope. Replayed to new clients on
35
+ // connect so they can reconstruct the current committed state without a
36
+ // separate snapshot mechanism.
37
+ const committedLog = [];
38
+ let nextTime = 1;
39
+ const handleMessage = (origin, msg) => {
40
+ // Record liveness for any inbound message — pings are explicit, but a
41
+ // chatty client that's actively sending proposals also counts as alive.
42
+ lastInboundAt.set(origin, Date.now());
43
+ if (msg.kind === "ping") {
44
+ origin.send({ kind: "pong" });
45
+ return;
46
+ }
47
+ if (msg.kind === "hello") {
48
+ const clientSessionId = msg.sessionId;
49
+ const lastAppliedTime = msg.lastAppliedTime;
50
+ const maxServerTime = committedLog.length > 0 ? committedLog[committedLog.length - 1].time : 0;
51
+ // A fresh client (no prior session id) always gets the full log with
52
+ // resetRequired:false — its database is empty so no reset is needed.
53
+ // A returning client with a mismatched session or a watermark ahead
54
+ // of the server must reset and replay from scratch.
55
+ const sessionMismatch = clientSessionId !== undefined && clientSessionId !== sessionId;
56
+ const clientAhead = lastAppliedTime > maxServerTime;
57
+ const resetRequired = sessionMismatch || clientAhead;
58
+ const replayCount = committedLog.filter(e => e.time > (resetRequired ? 0 : lastAppliedTime)).length;
59
+ log(`hello in (priorSession=${clientSessionId ?? "<fresh>"}, watermark=${lastAppliedTime}) → welcome (resetRequired=${resetRequired}, replaying=${replayCount})`);
60
+ origin.send({ kind: "welcome", sessionId, resetRequired });
61
+ // Replay the tail the client is missing (or the full log if reset needed).
62
+ const fromTime = resetRequired ? 0 : lastAppliedTime;
63
+ for (const envelope of committedLog) {
64
+ if (envelope.time > fromTime) {
65
+ origin.send({ kind: "committed", envelope });
66
+ }
67
+ }
68
+ }
69
+ else if (msg.kind === "propose") {
70
+ const committed = {
71
+ ...msg.envelope,
72
+ time: nextTime++,
73
+ };
74
+ committedLog.push(committed);
75
+ log(`propose in (id=${msg.envelope.id}, name=${msg.envelope.name}) → committed at time=${committed.time}, broadcast to ${clients.size}`);
76
+ // Echo to ALL clients (including proposer, which promotes its transient).
77
+ for (const c of clients) {
78
+ c.send({ kind: "committed", envelope: committed });
79
+ }
80
+ }
81
+ else if (msg.kind === "cancel") {
82
+ log(`cancel in (id=${msg.id}) → broadcast to ${clients.size}`);
83
+ for (const c of clients) {
84
+ c.send({ kind: "cancelled", id: msg.id });
85
+ }
86
+ }
87
+ else if (msg.kind === "intermediate") {
88
+ // Speculative intermediate — relay to peers, never logged.
89
+ for (const c of clients) {
90
+ if (c !== origin)
91
+ c.send({ kind: "committed", envelope: { ...msg.envelope } });
92
+ }
93
+ }
94
+ };
95
+ const connect = (transport) => {
96
+ // Add to the live set immediately so live commits during the replay
97
+ // window are forwarded to this client too.
98
+ clients.add(transport);
99
+ lastInboundAt.set(transport, Date.now());
100
+ log(`client connected (now ${clients.size} client${clients.size === 1 ? "" : "s"})`);
101
+ // Do NOT replay history here — wait for the client's `hello` which
102
+ // tells us what tail (if any) it already has.
103
+ const unsub = transport.onMessage(msg => handleMessage(transport, msg));
104
+ unsubscribers.set(transport, [unsub]);
105
+ return () => {
106
+ for (const fn of unsubscribers.get(transport) ?? [])
107
+ fn();
108
+ unsubscribers.delete(transport);
109
+ clients.delete(transport);
110
+ lastInboundAt.delete(transport);
111
+ transport.close();
112
+ };
113
+ };
114
+ // Per-client liveness sweep. We close transports that haven't sent any
115
+ // inbound traffic for longer than livenessTimeoutMs; closing fires the
116
+ // host-side onClose listeners so the negotiation layer can drive its
117
+ // disconnect/reconnect flow.
118
+ let livenessTimer;
119
+ if (livenessTimeoutMs > 0) {
120
+ const checkInterval = Math.max(50, Math.floor(livenessTimeoutMs / 4));
121
+ livenessTimer = setInterval(() => {
122
+ const now = Date.now();
123
+ for (const transport of clients) {
124
+ const last = lastInboundAt.get(transport) ?? now;
125
+ if (now - last > livenessTimeoutMs) {
126
+ log(`client liveness timeout — disconnecting (${now - last}ms silent)`);
127
+ // close() triggers our own onClose paths via the transport
128
+ // adapter, but we also clean the local maps eagerly.
129
+ clients.delete(transport);
130
+ lastInboundAt.delete(transport);
131
+ for (const fn of unsubscribers.get(transport) ?? [])
132
+ fn();
133
+ unsubscribers.delete(transport);
134
+ transport.close();
135
+ }
136
+ }
137
+ }, checkInterval);
138
+ }
139
+ const dispose = () => {
140
+ log(`dispose (${clients.size} client${clients.size === 1 ? "" : "s"})`);
141
+ if (livenessTimer !== undefined) {
142
+ clearInterval(livenessTimer);
143
+ livenessTimer = undefined;
144
+ }
145
+ for (const transport of clients) {
146
+ for (const fn of unsubscribers.get(transport) ?? [])
147
+ fn();
148
+ transport.close();
149
+ }
150
+ clients.clear();
151
+ unsubscribers.clear();
152
+ lastInboundAt.clear();
153
+ };
154
+ return { sessionId, connect, dispose };
155
+ };
156
+ //# sourceMappingURL=create-sync-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-sync-server.js","sourceRoot":"","sources":["../src/create-sync-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ import { type Database } from "@adobe/data/ecs";
2
+ import type { ClientTransport } from "./transport.js";
3
+ /**
4
+ * Options for {@link createSyncService}.
5
+ */
6
+ export interface SyncServiceOptions {
7
+ /**
8
+ * The database to wire into the sync layer. Must have been created with
9
+ * `createRebaseReplayConcurrency(userId)` as its concurrency strategy,
10
+ * where `userId` is unique per peer — the reconciler's transient queue
11
+ * uses `(userId, id)` as its compound key to keep concurrent peers from
12
+ * colliding, and the deferred-commit semantics are required for cross-peer
13
+ * entity-id determinism.
14
+ */
15
+ readonly database: Database<any, any, any, any>;
16
+ /** Transport connecting this peer to the sync server. */
17
+ readonly transport: ClientTransport;
18
+ /**
19
+ * Maximum `"intermediate"` messages forwarded per second. Intermediate
20
+ * samples beyond this rate are silently dropped. Commits and cancels
21
+ * are always forwarded immediately.
22
+ *
23
+ * Defaults to 20 (one per ~50 ms).
24
+ */
25
+ readonly maxIntermediatesPerSecond?: number;
26
+ /**
27
+ * Session id received from a previous connection's `welcome` message.
28
+ * Omit (or pass `undefined`) for a fresh client. If the server echoes back
29
+ * the same id in `welcome`, it replays only the tail; otherwise it signals
30
+ * a full reset and replay.
31
+ */
32
+ readonly priorSessionId?: string;
33
+ /**
34
+ * Highest committed `time` already applied to `database` from a previous
35
+ * connection. The server uses this as the replay start point when the
36
+ * session matches. Defaults to `0` (full replay).
37
+ */
38
+ readonly initialWatermark?: number;
39
+ /**
40
+ * Called once when the server's `welcome` message arrives, before any
41
+ * replay envelopes are applied. Use this to call `database.reset()` when
42
+ * `resetRequired` is true. Because it runs before the replay buffer is
43
+ * flushed, it is safe to reset the database inside this callback.
44
+ */
45
+ readonly onWelcome?: (msg: {
46
+ sessionId: string;
47
+ resetRequired: boolean;
48
+ }) => void;
49
+ /**
50
+ * Interval between outbound `ping` keep-alives, in milliseconds. Pings
51
+ * keep intermediate NAT mappings warm and let the peer detect a silent
52
+ * drop faster than the underlying transport's own heartbeat. Pass `0` to
53
+ * disable ping sending (the service will still observe inbound traffic
54
+ * for liveness if `livenessTimeoutMs > 0`). Defaults to `10_000`.
55
+ */
56
+ readonly pingIntervalMs?: number;
57
+ /**
58
+ * Maximum gap between inbound messages before the transport is considered
59
+ * dead. When exceeded the service calls `transport.close()`, which fires
60
+ * the transport's `onClose` listeners — application code typically wires
61
+ * those to a reconnect flow. Pass `0` to disable liveness checking.
62
+ * Defaults to `25_000`.
63
+ */
64
+ readonly livenessTimeoutMs?: number;
65
+ /**
66
+ * Optional sink for human-readable lifecycle messages — handshake,
67
+ * commits, cancels, liveness events, dispose. Pings/pongs are
68
+ * intentionally not logged (too noisy). The library itself never imports
69
+ * `console`, so consumers that want visibility wire one up explicitly:
70
+ *
71
+ * logger: (m) => console.log(`[sync] ${m}`)
72
+ */
73
+ readonly logger?: (msg: string) => void;
74
+ }
75
+ /**
76
+ * Background service returned by {@link createSyncService}. It owns no public
77
+ * mutation surface — all mutations flow through `db.transactions.X(args)`.
78
+ */
79
+ export interface SyncService {
80
+ /** Stop forwarding, unsubscribe from the transport, and close it. */
81
+ readonly dispose: () => void;
82
+ /** The highest committed `time` applied to the database so far. */
83
+ readonly lastAppliedTime: () => number;
84
+ /** The session id received from the server's `welcome` message, or `undefined` if not yet connected. */
85
+ readonly sessionId: () => string | undefined;
86
+ }
87
+ /**
88
+ * Wires a {@link Database} to a {@link ClientTransport} so that every
89
+ * locally-initiated transaction is automatically replicated to peers, and
90
+ * every server-broadcast envelope is automatically applied to the local DB.
91
+ *
92
+ * The service is a pure subscriber — application code only ever calls
93
+ * `db.transactions.X(args)`. It mirrors the shape of
94
+ * `createStoragePersistenceService`: side-effect lifecycle attached to a DB.
95
+ *
96
+ * Forwarding rules:
97
+ * - `db.observe.envelopes` fires once per locally-initiated envelope (the
98
+ * wrapper notifies it after `applyEnvelope` returns). Replays inside the
99
+ * reconciler and inbound `db.apply()` calls do NOT fire it, so there is
100
+ * no bounce-back to filter.
101
+ * - Envelopes whose `TransactionResult.persistent === false` (i.e. they only
102
+ * touched non-persistent resources/entities) are skipped. UI-only state lives
103
+ * inside the same database without ever reaching the wire.
104
+ * - `time > 0` → `kind: "propose"` (reliable).
105
+ * - `time < 0` → `kind: "intermediate"` (rate-limited, lossy).
106
+ * - `time === 0` → `kind: "cancel"` (reliable).
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const db = Database.create(myPlugin, {
111
+ * concurrency: createRebaseReplayConcurrency(crypto.randomUUID()),
112
+ * });
113
+ * const sync = createSyncService({ database: db, transport: ws });
114
+ *
115
+ * db.transactions.spawnUnit({ x: 0, y: 0, hp: 100 });
116
+ *
117
+ * sync.dispose();
118
+ * ```
119
+ */
120
+ export declare const createSyncService: (options: SyncServiceOptions) => SyncService;
@@ -0,0 +1,192 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { Entity } from "@adobe/data/ecs";
3
+ /**
4
+ * Wires a {@link Database} to a {@link ClientTransport} so that every
5
+ * locally-initiated transaction is automatically replicated to peers, and
6
+ * every server-broadcast envelope is automatically applied to the local DB.
7
+ *
8
+ * The service is a pure subscriber — application code only ever calls
9
+ * `db.transactions.X(args)`. It mirrors the shape of
10
+ * `createStoragePersistenceService`: side-effect lifecycle attached to a DB.
11
+ *
12
+ * Forwarding rules:
13
+ * - `db.observe.envelopes` fires once per locally-initiated envelope (the
14
+ * wrapper notifies it after `applyEnvelope` returns). Replays inside the
15
+ * reconciler and inbound `db.apply()` calls do NOT fire it, so there is
16
+ * no bounce-back to filter.
17
+ * - Envelopes whose `TransactionResult.persistent === false` (i.e. they only
18
+ * touched non-persistent resources/entities) are skipped. UI-only state lives
19
+ * inside the same database without ever reaching the wire.
20
+ * - `time > 0` → `kind: "propose"` (reliable).
21
+ * - `time < 0` → `kind: "intermediate"` (rate-limited, lossy).
22
+ * - `time === 0` → `kind: "cancel"` (reliable).
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const db = Database.create(myPlugin, {
27
+ * concurrency: createRebaseReplayConcurrency(crypto.randomUUID()),
28
+ * });
29
+ * const sync = createSyncService({ database: db, transport: ws });
30
+ *
31
+ * db.transactions.spawnUnit({ x: 0, y: 0, hp: 100 });
32
+ *
33
+ * sync.dispose();
34
+ * ```
35
+ */
36
+ export const createSyncService = (options) => {
37
+ const { database, transport, maxIntermediatesPerSecond = 20, priorSessionId, initialWatermark = 0, onWelcome, pingIntervalMs = 10_000, livenessTimeoutMs = 25_000, logger, } = options;
38
+ const log = logger ?? (() => undefined);
39
+ if (!database.concurrency.deferredCommit || database.concurrency.userId === undefined) {
40
+ throw new Error("createSyncService: database must use createRebaseReplayConcurrency(userId) as its concurrency strategy.");
41
+ }
42
+ const minIntervalMs = 1000 / maxIntermediatesPerSecond;
43
+ let lastIntermediateSentAt = 0;
44
+ let lastAppliedTime = initialWatermark;
45
+ let serverSessionId;
46
+ let lastInboundAt = Date.now();
47
+ let pingTimer;
48
+ let livenessTimer;
49
+ const stopHeartbeat = () => {
50
+ if (pingTimer !== undefined) {
51
+ clearInterval(pingTimer);
52
+ pingTimer = undefined;
53
+ }
54
+ if (livenessTimer !== undefined) {
55
+ clearInterval(livenessTimer);
56
+ livenessTimer = undefined;
57
+ }
58
+ };
59
+ // Buffer inbound committed/cancelled envelopes that arrive before the
60
+ // welcome handler runs. This lets onWelcome call database.reset()
61
+ // synchronously before any replayed envelopes are applied.
62
+ let bufferedInbound = [];
63
+ const flushBuffered = () => {
64
+ if (bufferedInbound === null)
65
+ return;
66
+ const msgs = bufferedInbound;
67
+ bufferedInbound = null;
68
+ for (const msg of msgs)
69
+ applyInbound(msg);
70
+ };
71
+ const applyInbound = (msg) => {
72
+ if (msg.kind === "committed") {
73
+ log(`committed in (time=${msg.envelope.time}, name=${msg.envelope.name})`);
74
+ database.apply(msg.envelope);
75
+ if (msg.envelope.time > lastAppliedTime) {
76
+ lastAppliedTime = msg.envelope.time;
77
+ }
78
+ }
79
+ else if (msg.kind === "cancelled") {
80
+ log(`cancelled in (id=${msg.id})`);
81
+ database.cancel(msg.id);
82
+ }
83
+ };
84
+ const unsubscribeInbound = transport.onMessage((msg) => {
85
+ // Any inbound traffic — committed envelope, welcome, pong, anything —
86
+ // proves the peer is still alive. Refresh the liveness deadline first
87
+ // so we don't trip over slow callbacks below.
88
+ lastInboundAt = Date.now();
89
+ if (msg.kind === "pong")
90
+ return;
91
+ if (msg.kind === "welcome") {
92
+ log(`welcome in (sessionId=${msg.sessionId}, resetRequired=${msg.resetRequired})`);
93
+ serverSessionId = msg.sessionId;
94
+ // Invoke the welcome handler synchronously (it may call database.reset()).
95
+ onWelcome?.({ sessionId: msg.sessionId, resetRequired: msg.resetRequired });
96
+ // After the handler returns, flush buffered replay envelopes.
97
+ flushBuffered();
98
+ }
99
+ else if (bufferedInbound !== null) {
100
+ bufferedInbound.push(msg);
101
+ }
102
+ else {
103
+ applyInbound(msg);
104
+ }
105
+ });
106
+ // A component whose SCHEMA is marked nonShared is local to this client.
107
+ const isNonSharedComponent = (component) => database.componentSchemas[component]?.nonShared === true;
108
+ // Whether a transaction has any effect that must reach peers: it changed a
109
+ // shared entity through a shared (not nonShared) component. A transaction
110
+ // whose effects are entirely non-shared — only non-shared entities, or only
111
+ // nonShared components — stays local. NOTE: peers replay the WHOLE
112
+ // transaction, so a mixed shared/non-shared transaction cannot be partially
113
+ // stripped; keep non-shared mutations in their own transactions to keep them
114
+ // off the wire.
115
+ const hasSharedEffect = (result) => {
116
+ let sharedEntity = false;
117
+ for (const entity of result.changedEntities.keys()) {
118
+ if (Entity.isShared(entity)) {
119
+ sharedEntity = true;
120
+ break;
121
+ }
122
+ }
123
+ if (!sharedEntity)
124
+ return false;
125
+ for (const component of result.changedComponents) {
126
+ if (!isNonSharedComponent(component))
127
+ return true;
128
+ }
129
+ return false;
130
+ };
131
+ const unsubscribeOutbound = database.observe.envelopes(({ envelope, result, intent }) => {
132
+ // Skip envelopes whose only effect was on non-persistent entities/
133
+ // resources, or entirely on non-shared (client-local) entities/components.
134
+ if (result !== undefined && (!result.persistent || !hasSharedEffect(result)))
135
+ return;
136
+ if (intent === "commit") {
137
+ log(`propose out (id=${envelope.id}, name=${envelope.name})`);
138
+ transport.send({ kind: "propose", envelope });
139
+ }
140
+ else if (intent === "intermediate") {
141
+ const now = Date.now();
142
+ if (now - lastIntermediateSentAt < minIntervalMs)
143
+ return;
144
+ lastIntermediateSentAt = now;
145
+ transport.send({ kind: "intermediate", envelope });
146
+ }
147
+ else {
148
+ log(`cancel out (id=${envelope.id})`);
149
+ transport.send({ kind: "cancel", id: envelope.id });
150
+ }
151
+ });
152
+ // Send hello immediately — the onWelcome option is already registered, so
153
+ // the welcome response can be processed synchronously even on loopback.
154
+ log(`hello out (priorSessionId=${priorSessionId ?? "<fresh>"}, watermark=${initialWatermark})`);
155
+ transport.send({ kind: "hello", sessionId: priorSessionId, lastAppliedTime: initialWatermark });
156
+ // Keep-alive timers. The transport's own onClose listener clears them as
157
+ // a safety net so a leaked service doesn't keep the event loop alive.
158
+ if (pingIntervalMs > 0) {
159
+ pingTimer = setInterval(() => {
160
+ transport.send({ kind: "ping" });
161
+ }, pingIntervalMs);
162
+ }
163
+ if (livenessTimeoutMs > 0) {
164
+ // Check at a fraction of the timeout so we detect a drop within
165
+ // ~timeout + checkInterval and tests can use small timeouts.
166
+ const checkInterval = Math.max(50, Math.floor(livenessTimeoutMs / 4));
167
+ livenessTimer = setInterval(() => {
168
+ if (Date.now() - lastInboundAt > livenessTimeoutMs) {
169
+ log(`liveness timeout — closing transport`);
170
+ stopHeartbeat();
171
+ transport.close();
172
+ }
173
+ }, checkInterval);
174
+ }
175
+ const unsubscribeClose = transport.onClose(() => {
176
+ log(`transport closed`);
177
+ stopHeartbeat();
178
+ });
179
+ return {
180
+ dispose: () => {
181
+ log(`dispose`);
182
+ stopHeartbeat();
183
+ unsubscribeClose();
184
+ unsubscribeOutbound();
185
+ unsubscribeInbound();
186
+ transport.close();
187
+ },
188
+ lastAppliedTime: () => lastAppliedTime,
189
+ sessionId: () => serverSessionId,
190
+ };
191
+ };
192
+ //# sourceMappingURL=create-sync-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-sync-service.js","sourceRoot":"","sources":["../src/create-sync-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type { SyncTransport, ClientTransport, ServerTransport, ServerMessage, ClientMessage, } from "./transport.js";
2
+ export { createLoopbackTransport } from "./loopback-transport.js";
3
+ export type { SyncServer, SyncServerOptions } from "./create-sync-server.js";
4
+ export { createSyncServer } from "./create-sync-server.js";
5
+ export type { SyncService, SyncServiceOptions } from "./create-sync-service.js";
6
+ export { createSyncService } from "./create-sync-service.js";
7
+ export type { WebSocketClientTransportOptions, WebSocketLike, } from "./websocket-transport.js";
8
+ export { createWebSocketClientTransport, createWebSocketServerTransport, } from "./websocket-transport.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ export { createLoopbackTransport } from "./loopback-transport.js";
3
+ export { createSyncServer } from "./create-sync-server.js";
4
+ export { createSyncService } from "./create-sync-service.js";
5
+ export { createWebSocketClientTransport, createWebSocketServerTransport, } from "./websocket-transport.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAUvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAM7D,OAAO,EACH,8BAA8B,EAC9B,8BAA8B,GACjC,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};