@voltro/local-first 0.29.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/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ import { a as e, c as t, i as n, n as r, o as i, r as a, s as o, t as s } from "./room-CzWRYppA.js";
2
+ import * as c from "yjs";
3
+ //#region src/crdt/yjsBackend.ts
4
+ var l = "t", u = (e) => c.encodeStateAsUpdate(e), d = (e) => {
5
+ let t = new c.Doc();
6
+ return e.length > 0 && c.applyUpdate(t, e), t;
7
+ }, f = (e) => {
8
+ let t = e.getText(l);
9
+ return {
10
+ toString: () => t.toString(),
11
+ insert: (e, n) => t.insert(e, n),
12
+ delete: (e, n) => t.delete(e, n),
13
+ encodeState: () => u(e),
14
+ applyState: (t) => {
15
+ t.length > 0 && c.applyUpdate(e, t);
16
+ }
17
+ };
18
+ }, p = {
19
+ name: "yjs",
20
+ createText: (e) => {
21
+ let t = new c.Doc();
22
+ return e !== void 0 && e.length > 0 && t.getText(l).insert(0, e), f(t);
23
+ },
24
+ merge: (e, t) => c.mergeUpdates([e, t]),
25
+ decodeText: (e) => {
26
+ let t = d(e), n = t.getText(l).toString();
27
+ return t.destroy(), n;
28
+ },
29
+ emptyState: () => u(new c.Doc())
30
+ }, m = p, h = (e, t = m) => {
31
+ let n = t.createText(e), r = {
32
+ backend: t.name,
33
+ toString: () => n.toString(),
34
+ insert: (e, t) => (n.insert(e, t), r),
35
+ delete: (e, t) => (n.delete(e, t), r),
36
+ merge: (e) => (n.applyState(e), r),
37
+ encode: () => n.encodeState()
38
+ };
39
+ return r;
40
+ }, g = (e, t, n = m) => n.merge(e, t), _ = (e, t = m) => t.decodeText(e), v = (e = m) => e.emptyState(), y = (e) => {
41
+ if (e.writer !== void 0) return `w:${e.writer}`;
42
+ try {
43
+ return `v:${JSON.stringify(e.value)}`;
44
+ } catch {
45
+ return `v:${String(e.value)}`;
46
+ }
47
+ }, b = (e, t) => t.updatedAt > e.updatedAt ? t.value : t.updatedAt < e.updatedAt ? e.value : y(t) > y(e) ? t.value : e.value, x = (e) => e === "lastWriteWins" ? b : e, S = (e) => {
48
+ let t = (t, n, r) => {
49
+ let i = e[t];
50
+ return (i === void 0 ? b : x(i))(n, r);
51
+ };
52
+ return {
53
+ resolveField: t,
54
+ resolveRecord: (e, n) => {
55
+ let r = {}, i = /* @__PURE__ */ new Set([...Object.keys(e), ...Object.keys(n)]);
56
+ for (let a of i) {
57
+ let i = e[a], o = n[a];
58
+ if (i === void 0) {
59
+ o !== void 0 && (r[a] = o.value);
60
+ continue;
61
+ }
62
+ if (o === void 0) {
63
+ r[a] = i.value;
64
+ continue;
65
+ }
66
+ r[a] = t(a, i, o);
67
+ }
68
+ return r;
69
+ }
70
+ };
71
+ }, C = (e, t) => `${e}${t}`, w = () => {
72
+ let e = /* @__PURE__ */ new Map(), t = [], n = /* @__PURE__ */ new Set(), r = (e) => e.slice();
73
+ return {
74
+ kind: "in-memory",
75
+ loadCrdtState: (t, n) => {
76
+ let i = e.get(C(t, n));
77
+ return Promise.resolve(i === void 0 ? void 0 : r(i));
78
+ },
79
+ saveCrdtState: (t, n, i) => (e.set(C(t, n), r(i)), Promise.resolve()),
80
+ loadQueue: () => Promise.resolve(t.map((e) => ({ ...e }))),
81
+ saveQueue: (e) => (t = e.map((e) => ({ ...e })), Promise.resolve()),
82
+ pin: (e, t) => (n.add(C(e, t)), Promise.resolve()),
83
+ isPinned: (e, t) => Promise.resolve(n.has(C(e, t)))
84
+ };
85
+ }, T = "voltro_local_first", E = "\0queue", D = "\0pin\0", O = (e, t) => `cell\u0000${e}\u0000${t}`, k = (e, t) => `${D}${e}\u0000${t}`, A = (e) => new Promise((t, n) => {
86
+ e.onsuccess = () => t(e.result), e.onerror = () => n(e.error ?? /* @__PURE__ */ Error("IndexedDB request failed"));
87
+ }), j = async (e) => {
88
+ let t = e?.factory ?? globalThis.indexedDB;
89
+ if (t === void 0) throw Error("createIndexedDbPersistence: no IndexedDB available. Pass options.factory (e.g. in a non-browser host) or use createInMemoryPersistence().");
90
+ let n = e?.databaseName ?? "voltro-local-first", r = t.open(n, 1), i = await new Promise((e, t) => {
91
+ r.onupgradeneeded = () => {
92
+ let e = r.result;
93
+ e.objectStoreNames.contains(T) || e.createObjectStore(T);
94
+ }, r.onsuccess = () => e(r.result), r.onerror = () => t(r.error ?? /* @__PURE__ */ Error("IndexedDB open failed"));
95
+ }), a = async (e) => A(i.transaction(T, "readonly").objectStore(T).get(e)), o = async (e, t) => {
96
+ await A(i.transaction(T, "readwrite").objectStore(T).put(t, e));
97
+ }, s = (e) => {
98
+ if (e instanceof Uint8Array) return e.slice();
99
+ if (e instanceof ArrayBuffer) return new Uint8Array(e.slice(0));
100
+ if (Array.isArray(e)) return Uint8Array.from(e);
101
+ };
102
+ return {
103
+ kind: "indexeddb",
104
+ loadCrdtState: async (e, t) => s(await a(O(e, t))),
105
+ saveCrdtState: async (e, t, n) => {
106
+ await o(O(e, t), n.slice());
107
+ },
108
+ loadQueue: async () => {
109
+ let e = await a(E);
110
+ return e === void 0 || !Array.isArray(e.writes) ? [] : e.writes.map((e) => ({ ...e }));
111
+ },
112
+ saveQueue: async (e) => {
113
+ let t = { writes: e.map((e) => ({ ...e })) };
114
+ await o(E, t);
115
+ },
116
+ pin: async (e, t) => {
117
+ await o(k(e, t), !0);
118
+ },
119
+ isPinned: async (e, t) => await a(k(e, t)) === !0
120
+ };
121
+ }, M = (e) => e.inFlight === void 0 ? e.pending : [e.inFlight, ...e.pending], N = async (e, n = !0) => {
122
+ let r = await e.loadQueue(), a = {
123
+ ...i(n),
124
+ pending: r
125
+ };
126
+ return {
127
+ getState: () => a,
128
+ dispatch: async (n) => (a = t(a, n), await e.saveQueue(M(a)), a)
129
+ };
130
+ }, P = (e) => `${e.table}\u0000${e.id}\u0000${e.column}`, F = 0, I = () => {
131
+ let e = typeof crypto < "u" ? crypto : void 0;
132
+ return e && typeof e.randomUUID == "function" ? e.randomUUID() : `w_${Date.now().toString(36)}_${F += 1}`;
133
+ }, L = (n) => {
134
+ let { transport: r, adapter: a } = n, s = n.backend ?? m, c = n.now ?? Date.now, l = n.maxAttempts ?? 5, u = i(n.online ?? !0), d = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Set(), p = () => {
135
+ for (let e of f) try {
136
+ e();
137
+ } catch {}
138
+ }, h = (e, t) => {
139
+ let n = P(e), r = d.get(n), i = r === void 0 ? t : g(r, t, s);
140
+ d.set(n, i), a?.saveCrdtState(e.table, e.id, i).catch(() => void 0);
141
+ }, _ = () => {
142
+ if (a === void 0) return;
143
+ let e = u.inFlight === void 0 ? u.pending : [u.inFlight, ...u.pending];
144
+ a.saveQueue(e).catch(() => void 0);
145
+ }, v = !1, y = async () => {
146
+ if (!v) {
147
+ v = !0;
148
+ try {
149
+ for (; e(u);) {
150
+ u = t(u, { type: "drain-start" });
151
+ let e = u.inFlight;
152
+ if (e === void 0) break;
153
+ try {
154
+ await r.push(e), u = t(u, {
155
+ type: "ack",
156
+ id: e.id
157
+ }), _(), p();
158
+ } catch (n) {
159
+ if (u = t(u, {
160
+ type: "fail",
161
+ id: e.id,
162
+ error: n instanceof Error ? n.message : String(n)
163
+ }), _(), p(), e.attempts + 1 >= l) break;
164
+ }
165
+ }
166
+ } finally {
167
+ v = !1;
168
+ }
169
+ }
170
+ }, b = r.onRemoteState((e) => {
171
+ h(e, e.state), p();
172
+ });
173
+ return {
174
+ enqueue: (e) => {
175
+ h(e, e.update);
176
+ let n = {
177
+ id: I(),
178
+ payload: e,
179
+ enqueuedAt: c(),
180
+ attempts: 0
181
+ };
182
+ u = t(u, {
183
+ type: "enqueue",
184
+ write: n
185
+ }), _(), p(), y();
186
+ },
187
+ setOnline: (e) => {
188
+ u = t(u, e ? { type: "online" } : { type: "offline" }), _(), p(), e && y();
189
+ },
190
+ queueState: () => u,
191
+ outstanding: () => o(u),
192
+ getState: (e) => {
193
+ let t = d.get(P(e));
194
+ return t === void 0 ? void 0 : t.slice();
195
+ },
196
+ getText: (e) => {
197
+ let t = d.get(P(e));
198
+ return t === void 0 ? "" : s.decodeText(t);
199
+ },
200
+ subscribe: (e) => (f.add(e), () => {
201
+ f.delete(e);
202
+ }),
203
+ flush: async () => {
204
+ await y();
205
+ },
206
+ close: () => {
207
+ b(), f.clear();
208
+ }
209
+ };
210
+ }, R = () => ({ rooms: /* @__PURE__ */ new Map() }), z = (e = R()) => {
211
+ let t = /* @__PURE__ */ new Set(), n = (t) => {
212
+ let n = e.rooms.get(t);
213
+ return n === void 0 && (n = /* @__PURE__ */ new Set(), e.rooms.set(t, n)), n;
214
+ };
215
+ return {
216
+ kind: "presence-channel",
217
+ publish: (n, r) => {
218
+ let i = e.rooms.get(n);
219
+ if (i !== void 0) {
220
+ for (let e of i) if (!t.has(e)) try {
221
+ e(r);
222
+ } catch {}
223
+ }
224
+ },
225
+ subscribe: (e, r) => {
226
+ let i = n(e);
227
+ return i.add(r), t.add(r), () => {
228
+ i.delete(r), t.delete(r);
229
+ };
230
+ }
231
+ };
232
+ }, B = [
233
+ "durable-persistence-indexeddb",
234
+ "sync-transport-syncclient",
235
+ "presence-room",
236
+ "localFirst-mixin-discovery"
237
+ ], V = [
238
+ "sync-transport-app-binding",
239
+ "presence-broker-binding",
240
+ "wasm-sqlite-durable-adapter"
241
+ ];
242
+ //#endregion
243
+ export { B as IMPLEMENTED_SINCE_SEAMS, V as RUNTIME_SEAMS, e as canDrain, S as conflictPolicy, r as connectionReducer, h as crdtText, w as createInMemoryPersistence, R as createInMemoryPresenceBus, z as createInMemoryPresenceChannel, j as createIndexedDbPersistence, s as createPresenceRoom, L as createSyncClient, _ as decodeCrdtText, m as defaultCrdtBackend, a as deriveSyncStatus, v as emptyCrdtState, n as initialConnection, i as initialSyncQueue, b as lastWriteWins, N as loadPersistedSyncQueue, g as mergeCrdtStates, o as outstandingCount, t as syncQueueReducer, p as yjsBackend };
@@ -0,0 +1,138 @@
1
+ declare type ConnectionPhase = 'online' | 'offline' | 'reconnecting';
2
+
3
+ declare interface ConnectionState {
4
+ readonly phase: ConnectionPhase;
5
+ /** Confirmation attempts made since the network last came back. 0 when online/offline. */
6
+ readonly attempt: number;
7
+ }
8
+
9
+ /**
10
+ * A dumb ephemeral pub/sub channel. Publish a string payload to a room;
11
+ * subscribe to a room and get every payload (from other peers) back.
12
+ *
13
+ * Mirrors `BroadcastProvider` so a runtime adapter can forward straight onto the
14
+ * app's broker. `publish` is fire-and-forget (presence loss is tolerable — the
15
+ * next heartbeat corrects it); `subscribe` returns its unsubscribe.
16
+ */
17
+ declare interface PresenceChannel {
18
+ readonly kind: 'presence-channel';
19
+ readonly publish: (room: string, payload: string) => void;
20
+ readonly subscribe: (room: string, handler: (payload: string) => void) => () => void;
21
+ }
22
+
23
+ /** One other peer's last-known presence. */
24
+ declare interface PresencePeer<TPresence> {
25
+ readonly peerId: string;
26
+ readonly presence: TPresence;
27
+ /** Clock value of the last message seen from this peer (for TTL expiry). */
28
+ readonly lastSeen: number;
29
+ }
30
+
31
+ /** A single queued write. `payload` is opaque to the queue — the wire seam owns its shape. */
32
+ declare interface QueuedWrite<T = unknown> {
33
+ readonly id: string;
34
+ readonly payload: T;
35
+ /** When the write was first enqueued (ms epoch). Set by the caller for determinism. */
36
+ readonly enqueuedAt: number;
37
+ /** How many times delivery has been attempted and failed. 0 until the first `fail`. */
38
+ readonly attempts: number;
39
+ }
40
+
41
+ declare interface SyncQueueState<T = unknown> {
42
+ /** Whether the queue believes it can reach the server. Drives draining. */
43
+ readonly online: boolean;
44
+ /** Writes waiting to be sent, oldest first. */
45
+ readonly pending: readonly QueuedWrite<T>[];
46
+ /** The write currently being delivered, if any. Only one at a time (in-order delivery). */
47
+ readonly inFlight: QueuedWrite<T> | undefined;
48
+ /** Last delivery error message, if the most recent attempt failed. */
49
+ readonly lastError: string | undefined;
50
+ }
51
+
52
+ /** What an offline indicator shows: a join of network phase and queue depth. */
53
+ declare type SyncStatus = 'offline' | 'syncing' | 'synced';
54
+
55
+ export declare interface UseConnectionStatus {
56
+ readonly state: ConnectionState;
57
+ readonly phase: ConnectionPhase;
58
+ /** Display status joining phase with `outstanding` writes: offline | syncing | synced. */
59
+ readonly status: SyncStatus;
60
+ /** Report a confirmed round-trip to the server (moves reconnecting → online). */
61
+ readonly confirm: () => void;
62
+ /** Report a failed confirmation (counts a reconnect attempt). */
63
+ readonly confirmFailed: () => void;
64
+ }
65
+
66
+ /**
67
+ * Observe network connection lifecycle for the local-first layer.
68
+ *
69
+ * @param outstanding number of unsynced writes (e.g. `useSyncQueue().outstanding`)
70
+ * — folded into the display status so `synced` means online AND drained.
71
+ *
72
+ * ```tsx
73
+ * const q = useSyncQueue()
74
+ * const { status } = useConnectionStatus(q.outstanding)
75
+ * {status !== 'synced' && <OfflineBanner status={status} />}
76
+ * ```
77
+ */
78
+ export declare const useConnectionStatus: (outstanding?: number) => UseConnectionStatus;
79
+
80
+ export declare interface UsePresence<TPresence> {
81
+ /** This peer's current presence. */
82
+ readonly presence: TPresence;
83
+ /** Every other live peer. */
84
+ readonly others: readonly PresencePeer<TPresence>[];
85
+ /** Replace this peer's presence and publish it. */
86
+ readonly setPresence: (presence: TPresence) => void;
87
+ }
88
+
89
+ /**
90
+ * Ephemeral multiplayer awareness for a document/room.
91
+ *
92
+ * ```tsx
93
+ * const { presence, others, setPresence } = usePresence(
94
+ * documentId,
95
+ * { cursor: editor.selection, name: currentUser.name },
96
+ * { channel },
97
+ * )
98
+ * // render `others` as remote cursors; call setPresence on selection change
99
+ * ```
100
+ */
101
+ export declare const usePresence: <TPresence>(roomId: string, initial: TPresence, options: UsePresenceOptions) => UsePresence<TPresence>;
102
+
103
+ export declare interface UsePresenceOptions {
104
+ /** The transport. In an app, injected from a broker-bound provider. */
105
+ readonly channel: PresenceChannel;
106
+ /** This peer's stable id. Defaults to a fresh per-mount id (one tab = one peer). */
107
+ readonly peerId?: string;
108
+ /** A peer silent this long (ms) is dropped. Default 30_000. */
109
+ readonly ttlMs?: number;
110
+ /** Heartbeat + sweep cadence (ms). Default 5_000. Kept well under `ttlMs`. */
111
+ readonly heartbeatMs?: number;
112
+ }
113
+
114
+ export declare interface UseSyncQueue<T = unknown> {
115
+ readonly state: SyncQueueState<T>;
116
+ /** Outstanding writes (pending + in-flight). 0 means synced. */
117
+ readonly outstanding: number;
118
+ /** Queue a write at the tail. */
119
+ readonly enqueue: (write: QueuedWrite<T>) => void;
120
+ readonly setOnline: (online: boolean) => void;
121
+ /** Move the head into flight (no-op unless online, idle, and non-empty). */
122
+ readonly drainNext: () => void;
123
+ readonly ack: (id: string) => void;
124
+ readonly fail: (id: string, error: string) => void;
125
+ }
126
+
127
+ /**
128
+ * Reactive view of an offline write queue.
129
+ *
130
+ * ```tsx
131
+ * const q = useSyncQueue()
132
+ * q.enqueue({ id, payload, enqueuedAt: Date.now(), attempts: 0 })
133
+ * // ... transport wiring calls q.drainNext() / q.ack(id) / q.fail(id, msg)
134
+ * ```
135
+ */
136
+ export declare const useSyncQueue: <T = unknown>(online?: boolean) => UseSyncQueue<T>;
137
+
138
+ export { }
package/dist/react.js ADDED
@@ -0,0 +1,102 @@
1
+ import { c as e, i as t, n, o as r, r as i, s as a, t as o } from "./room-CzWRYppA.js";
2
+ import { useCallback as s, useEffect as c, useMemo as l, useReducer as u, useRef as d, useState as f } from "react";
3
+ //#region src/react/useSyncQueue.ts
4
+ var p = (t = !0) => {
5
+ let [n, i] = u(e, t, r), o = s((e) => {
6
+ i({
7
+ type: "enqueue",
8
+ write: e
9
+ });
10
+ }, []), c = s((e) => {
11
+ i(e ? { type: "online" } : { type: "offline" });
12
+ }, []), d = s(() => i({ type: "drain-start" }), []), f = s((e) => i({
13
+ type: "ack",
14
+ id: e
15
+ }), []), p = s((e, t) => {
16
+ i({
17
+ type: "fail",
18
+ id: e,
19
+ error: t
20
+ });
21
+ }, []), m = a(n);
22
+ return l(() => ({
23
+ state: n,
24
+ outstanding: m,
25
+ enqueue: o,
26
+ setOnline: c,
27
+ drainNext: d,
28
+ ack: f,
29
+ fail: p
30
+ }), [
31
+ n,
32
+ m,
33
+ o,
34
+ c,
35
+ d,
36
+ f,
37
+ p
38
+ ]);
39
+ }, m = () => typeof navigator > "u" || navigator.onLine ? "online" : "offline", h = (e = 0) => {
40
+ let [r, a] = u(n, void 0, () => t(m()));
41
+ c(() => {
42
+ if (typeof window > "u") return;
43
+ let e = () => a({ type: "network-up" }), t = () => a({ type: "network-down" });
44
+ return window.addEventListener("online", e), window.addEventListener("offline", t), () => {
45
+ window.removeEventListener("online", e), window.removeEventListener("offline", t);
46
+ };
47
+ }, []);
48
+ let o = s(() => a({ type: "confirmed" }), []), d = s(() => a({ type: "confirm-failed" }), []), f = i(r.phase, e);
49
+ return l(() => ({
50
+ state: r,
51
+ phase: r.phase,
52
+ status: f,
53
+ confirm: o,
54
+ confirmFailed: d
55
+ }), [
56
+ r,
57
+ f,
58
+ o,
59
+ d
60
+ ]);
61
+ }, g = 0, _ = () => {
62
+ let e = typeof crypto < "u" ? crypto : void 0;
63
+ return e && typeof e.randomUUID == "function" ? e.randomUUID() : `peer_${Date.now().toString(36)}_${g += 1}`;
64
+ }, v = (e, t, n) => {
65
+ let { channel: r } = n, i = n.ttlMs ?? 3e4, a = n.heartbeatMs ?? 5e3, u = l(() => n.peerId ?? _(), [n.peerId]), p = d(null), [m, h] = f(0), g = d(t);
66
+ g.current = t, c(() => {
67
+ let t = o({
68
+ channel: r,
69
+ roomId: e,
70
+ peerId: u,
71
+ initial: g.current,
72
+ ttlMs: i
73
+ });
74
+ p.current = t;
75
+ let n = t.onChange(() => h((e) => e + 1)), s = setInterval(() => {
76
+ t.heartbeat(), t.sweep();
77
+ }, a);
78
+ return h((e) => e + 1), () => {
79
+ clearInterval(s), n(), t.leave(), p.current = null;
80
+ };
81
+ }, [
82
+ r,
83
+ e,
84
+ u,
85
+ i,
86
+ a
87
+ ]);
88
+ let v = s((e) => {
89
+ p.current?.update(e);
90
+ }, []), y = p.current;
91
+ return l(() => ({
92
+ presence: y?.self() ?? g.current,
93
+ others: y?.others() ?? [],
94
+ setPresence: v
95
+ }), [
96
+ y,
97
+ v,
98
+ m
99
+ ]);
100
+ };
101
+ //#endregion
102
+ export { h as useConnectionStatus, v as usePresence, p as useSyncQueue };
@@ -0,0 +1,134 @@
1
+ //#region src/sync/queue.ts
2
+ var e = (e = !0) => ({
3
+ online: e,
4
+ pending: [],
5
+ inFlight: void 0,
6
+ lastError: void 0
7
+ }), t = (e) => e.online && e.inFlight === void 0 && e.pending.length > 0, n = (e) => e.pending.length + (e.inFlight === void 0 ? 0 : 1), r = (e) => ({
8
+ ...e,
9
+ attempts: e.attempts + 1
10
+ }), i = (e, n) => {
11
+ switch (n.type) {
12
+ case "enqueue": return {
13
+ ...e,
14
+ pending: [...e.pending, n.write]
15
+ };
16
+ case "online": return e.online ? e : {
17
+ ...e,
18
+ online: !0
19
+ };
20
+ case "offline": return e.inFlight === void 0 ? e.online ? {
21
+ ...e,
22
+ online: !1
23
+ } : e : {
24
+ ...e,
25
+ online: !1,
26
+ pending: [e.inFlight, ...e.pending],
27
+ inFlight: void 0
28
+ };
29
+ case "drain-start": {
30
+ if (!t(e)) return e;
31
+ let [n, ...r] = e.pending;
32
+ return n === void 0 ? e : {
33
+ ...e,
34
+ pending: r,
35
+ inFlight: n,
36
+ lastError: void 0
37
+ };
38
+ }
39
+ case "ack": return e.inFlight === void 0 || e.inFlight.id !== n.id ? e : {
40
+ ...e,
41
+ inFlight: void 0,
42
+ lastError: void 0
43
+ };
44
+ case "fail": return e.inFlight === void 0 || e.inFlight.id !== n.id ? e : {
45
+ ...e,
46
+ pending: [r(e.inFlight), ...e.pending],
47
+ inFlight: void 0,
48
+ lastError: n.error
49
+ };
50
+ }
51
+ }, a = (e = "online") => ({
52
+ phase: e,
53
+ attempt: 0
54
+ }), o = (e, t) => {
55
+ switch (t.type) {
56
+ case "network-down": return e.phase === "offline" ? e : {
57
+ phase: "offline",
58
+ attempt: 0
59
+ };
60
+ case "network-up": return e.phase === "online" ? e : {
61
+ phase: "reconnecting",
62
+ attempt: 0
63
+ };
64
+ case "confirmed": return e.phase === "online" ? e : {
65
+ phase: "online",
66
+ attempt: 0
67
+ };
68
+ case "confirm-failed": return e.phase === "reconnecting" ? {
69
+ phase: "reconnecting",
70
+ attempt: e.attempt + 1
71
+ } : e;
72
+ }
73
+ }, s = (e, t) => e === "offline" ? "offline" : e === "reconnecting" || t > 0 ? "syncing" : "synced", c = (e) => {
74
+ let { channel: t, roomId: n, peerId: r, initial: i } = e, a = e.ttlMs ?? 3e4, o = e.now ?? Date.now, s = i, c = 0, l = !1, u = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Set(), p = () => {
75
+ for (let e of f) try {
76
+ e();
77
+ } catch {}
78
+ }, m = (e) => {
79
+ c += 1;
80
+ let i = e === "leave" ? {
81
+ t: e,
82
+ peerId: r,
83
+ seq: c
84
+ } : {
85
+ t: e,
86
+ peerId: r,
87
+ seq: c,
88
+ presence: s
89
+ };
90
+ t.publish(n, JSON.stringify(i));
91
+ }, h = t.subscribe(n, (e) => {
92
+ if (l) return;
93
+ let t;
94
+ try {
95
+ t = JSON.parse(e);
96
+ } catch {
97
+ return;
98
+ }
99
+ if (t.peerId === r) return;
100
+ if (t.t === "leave") {
101
+ d.delete(t.peerId), u.delete(t.peerId) && p();
102
+ return;
103
+ }
104
+ let n = u.has(t.peerId);
105
+ t.seq <= (d.get(t.peerId) ?? 0) || (d.set(t.peerId, t.seq), u.set(t.peerId, {
106
+ peerId: t.peerId,
107
+ presence: t.presence,
108
+ lastSeen: o()
109
+ }), n || m("update"), p());
110
+ });
111
+ return m("update"), {
112
+ self: () => s,
113
+ others: () => Array.from(u.values()),
114
+ update: (e) => {
115
+ s = e, m("update"), p();
116
+ },
117
+ heartbeat: () => {
118
+ l || m("update");
119
+ },
120
+ sweep: () => {
121
+ let e = o() - a, t = !1;
122
+ for (let [n, r] of u) r.lastSeen < e && (u.delete(n), t = !0);
123
+ t && p();
124
+ },
125
+ onChange: (e) => (f.add(e), () => {
126
+ f.delete(e);
127
+ }),
128
+ leave: () => {
129
+ l || (l = !0, m("leave"), h(), u.clear(), d.clear(), p(), f.clear());
130
+ }
131
+ };
132
+ };
133
+ //#endregion
134
+ export { t as a, i as c, a as i, o as n, e as o, s as r, n as s, c as t };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@voltro/local-first",
3
+ "version": "0.29.0",
4
+ "description": "Local-first / CRDT primitives (opt-in): a Yjs-backed crdtText() field and its deterministic merge, an offline sync-queue reducer, connection-status state machine, and last-write-wins conflict policy. Library-level primitives only — client SQLite persistence, the sync wire, and the presence channel are documented seams.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./react": {
27
+ "types": "./dist/react.d.ts",
28
+ "import": "./dist/react.js",
29
+ "default": "./dist/react.js"
30
+ }
31
+ },
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "sideEffects": false,
36
+ "engines": {
37
+ "node": ">=24.0.0"
38
+ },
39
+ "dependencies": {
40
+ "yjs": "^13.6.27"
41
+ },
42
+ "peerDependencies": {
43
+ "react": "^19.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "react": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }