meridian-react-native 0.4.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.cjs ADDED
@@ -0,0 +1,416 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createRNClient: () => createRNClient,
24
+ defineSchema: () => import_meridian_shared2.defineSchema,
25
+ useDoc: () => useDoc,
26
+ useLiveQuery: () => useLiveQuery,
27
+ useMutation: () => useMutation,
28
+ usePresence: () => usePresence,
29
+ useQuery: () => useQuery,
30
+ useQueryOptimized: () => useQueryOptimized,
31
+ useSync: () => useSync,
32
+ z: () => import_meridian_shared2.z
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/rn-client.ts
37
+ var import_meridian_shared = require("meridian-shared");
38
+ var AsyncKVStore = class {
39
+ memoryFallback = /* @__PURE__ */ new Map();
40
+ async get(key) {
41
+ try {
42
+ const AsyncStorage = require("@react-native-async-storage/async-storage").default;
43
+ return await AsyncStorage.getItem(`meridian:${key}`);
44
+ } catch {
45
+ return this.memoryFallback.get(key) ?? null;
46
+ }
47
+ }
48
+ async set(key, value) {
49
+ try {
50
+ const AsyncStorage = require("@react-native-async-storage/async-storage").default;
51
+ await AsyncStorage.setItem(`meridian:${key}`, value);
52
+ } catch {
53
+ this.memoryFallback.set(key, value);
54
+ }
55
+ }
56
+ async delete(key) {
57
+ try {
58
+ const AsyncStorage = require("@react-native-async-storage/async-storage").default;
59
+ await AsyncStorage.removeItem(`meridian:${key}`);
60
+ } catch {
61
+ this.memoryFallback.delete(key);
62
+ }
63
+ }
64
+ async keys() {
65
+ try {
66
+ const AsyncStorage = require("@react-native-async-storage/async-storage").default;
67
+ const allKeys = await AsyncStorage.getAllKeys();
68
+ return allKeys.filter((k) => k.startsWith("meridian:")).map((k) => k.replace("meridian:", ""));
69
+ } catch {
70
+ return Array.from(this.memoryFallback.keys());
71
+ }
72
+ }
73
+ };
74
+ function createRNClient(config) {
75
+ const { schema, serverUrl, auth, debug = false } = config;
76
+ const nodeId = (0, import_meridian_shared.generateNodeId)();
77
+ const clock = new import_meridian_shared.HLC(nodeId);
78
+ const store = new AsyncKVStore();
79
+ const collections = {};
80
+ let ws = null;
81
+ let state = "disconnected";
82
+ let destroyed = false;
83
+ for (const name of Object.keys(schema.collections)) {
84
+ const proxy = new RNCollectionProxy(name, store, clock, () => ws, serverUrl, auth, debug);
85
+ collections[name] = proxy;
86
+ }
87
+ connect().catch(() => {
88
+ });
89
+ async function connect() {
90
+ if (destroyed) return;
91
+ state = "connecting";
92
+ try {
93
+ ws = new WebSocket(serverUrl);
94
+ ws.onopen = async () => {
95
+ state = "connected";
96
+ if (auth) {
97
+ const token = await auth.getToken();
98
+ ws?.send(JSON.stringify({ type: "auth", token }));
99
+ }
100
+ };
101
+ ws.onmessage = (event) => {
102
+ try {
103
+ const msg = JSON.parse(event.data);
104
+ if (msg.type === "ack" && msg.opIds) {
105
+ for (const id of msg.opIds) {
106
+ store.delete(`pending:${id}`).catch(() => {
107
+ });
108
+ }
109
+ }
110
+ } catch {
111
+ }
112
+ };
113
+ ws.onerror = () => {
114
+ state = "error";
115
+ };
116
+ ws.onclose = () => {
117
+ state = "disconnected";
118
+ if (!destroyed) setTimeout(connect, 3e3);
119
+ };
120
+ } catch {
121
+ state = "disconnected";
122
+ if (!destroyed) setTimeout(connect, 5e3);
123
+ }
124
+ }
125
+ const client = {
126
+ get connectionState() {
127
+ return state;
128
+ },
129
+ async sync() {
130
+ if (ws && state === "connected") {
131
+ const pendingKeys = await store.keys();
132
+ const ops = [];
133
+ for (const key of pendingKeys) {
134
+ if (key.startsWith("pending:")) {
135
+ const opJson = await store.get(key);
136
+ if (opJson) ops.push(JSON.parse(opJson));
137
+ }
138
+ }
139
+ if (ops.length > 0) {
140
+ ws.send(JSON.stringify({ type: "push", ops }));
141
+ }
142
+ }
143
+ },
144
+ destroy() {
145
+ destroyed = true;
146
+ if (ws) {
147
+ ws.onclose = null;
148
+ ws.close();
149
+ ws = null;
150
+ }
151
+ }
152
+ };
153
+ for (const [name, proxy] of Object.entries(collections)) {
154
+ client[name] = proxy;
155
+ }
156
+ return client;
157
+ }
158
+ var RNCollectionProxy = class {
159
+ collection;
160
+ store;
161
+ clock;
162
+ getWs;
163
+ serverUrl;
164
+ auth;
165
+ debug;
166
+ constructor(collection, store, clock, getWs, serverUrl, auth, debug) {
167
+ this.collection = collection;
168
+ this.store = store;
169
+ this.clock = clock;
170
+ this.getWs = getWs;
171
+ this.serverUrl = serverUrl;
172
+ this.auth = auth;
173
+ this.debug = debug;
174
+ }
175
+ find(filter) {
176
+ const self = this;
177
+ const listeners = /* @__PURE__ */ new Set();
178
+ return {
179
+ subscribe(cb) {
180
+ listeners.add(cb);
181
+ self.queryDocs(filter).then(cb);
182
+ return () => {
183
+ listeners.delete(cb);
184
+ };
185
+ },
186
+ get: () => self.queryDocs(filter)
187
+ };
188
+ }
189
+ findOne(id) {
190
+ return {
191
+ subscribe: (cb) => {
192
+ this.getDoc(id).then(cb);
193
+ return () => {
194
+ };
195
+ },
196
+ get: () => this.getDoc(id)
197
+ };
198
+ }
199
+ live(options = {}) {
200
+ return this.find(options.where);
201
+ }
202
+ async put(doc) {
203
+ if (!doc.id) doc.id = `rn-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
204
+ const hlcTs = this.clock.now();
205
+ const hlcStr = `${hlcTs.wallTime}-${hlcTs.counter.toString().padStart(4, "0")}-${hlcTs.nodeId}`;
206
+ await this.store.set(`doc:${this.collection}:${doc.id}`, JSON.stringify(doc));
207
+ await this.queueOp(doc.id, Object.keys(doc).filter((k) => k !== "id"), doc, hlcStr);
208
+ this.trySync();
209
+ }
210
+ async update(id, fields) {
211
+ const existing = await this.getDoc(id);
212
+ if (!existing) return;
213
+ const merged = { ...existing, ...fields };
214
+ await this.store.set(`doc:${this.collection}:${id}`, JSON.stringify(merged));
215
+ const hlcTs = this.clock.now();
216
+ const hlcStr = `${hlcTs.wallTime}-${hlcTs.counter.toString().padStart(4, "0")}-${hlcTs.nodeId}`;
217
+ await this.queueOp(id, Object.keys(fields), fields, hlcStr);
218
+ this.trySync();
219
+ }
220
+ async delete(id) {
221
+ await this.store.set(`doc:${this.collection}:${id}:deleted`, "true");
222
+ this.trySync();
223
+ }
224
+ async queryDocs(filter) {
225
+ const allKeys = await this.store.keys();
226
+ const docs = [];
227
+ for (const key of allKeys) {
228
+ if (!key.startsWith(`doc:${this.collection}:`)) continue;
229
+ const id = key.split(":").pop();
230
+ const deleted = await this.store.get(`doc:${this.collection}:${id}:deleted`);
231
+ if (deleted === "true") continue;
232
+ const raw = await this.store.get(key);
233
+ if (!raw) continue;
234
+ const doc = JSON.parse(raw);
235
+ if (filter) {
236
+ let matches = true;
237
+ for (const [k, v] of Object.entries(filter)) {
238
+ if (doc[k] !== v) {
239
+ matches = false;
240
+ break;
241
+ }
242
+ }
243
+ if (!matches) continue;
244
+ }
245
+ docs.push(doc);
246
+ }
247
+ return docs;
248
+ }
249
+ async getDoc(id) {
250
+ const raw = await this.store.get(`doc:${this.collection}:${id}`);
251
+ if (!raw) return null;
252
+ return JSON.parse(raw);
253
+ }
254
+ async queueOp(docId, fields, values, hlc) {
255
+ for (const field of fields) {
256
+ const op = {
257
+ id: `${docId}-${field}-${hlc}`,
258
+ collection: this.collection,
259
+ docId,
260
+ field,
261
+ value: values[field],
262
+ hlc,
263
+ nodeId: this.clock.peek().nodeId
264
+ };
265
+ await this.store.set(`pending:${op.id}`, JSON.stringify(op));
266
+ }
267
+ }
268
+ trySync() {
269
+ const ws = this.getWs();
270
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
271
+ this.store.keys().then(async (keys) => {
272
+ const ops = [];
273
+ for (const key of keys) {
274
+ if (key.startsWith("pending:")) {
275
+ const raw = await this.store.get(key);
276
+ if (raw) ops.push(JSON.parse(raw));
277
+ }
278
+ }
279
+ if (ops.length > 0) {
280
+ ws.send(JSON.stringify({ type: "push", ops }));
281
+ }
282
+ });
283
+ }
284
+ };
285
+
286
+ // src/hooks.ts
287
+ var import_react = require("react");
288
+ function useQuery(query) {
289
+ const [data, setData] = (0, import_react.useState)();
290
+ (0, import_react.useEffect)(() => {
291
+ let mounted = true;
292
+ const unsub = query.subscribe((result) => {
293
+ if (mounted) setData(result);
294
+ });
295
+ return () => {
296
+ unsub();
297
+ mounted = false;
298
+ };
299
+ }, []);
300
+ return data;
301
+ }
302
+ function useLiveQuery(collection, options = {}) {
303
+ const [data, setData] = (0, import_react.useState)();
304
+ const { where: _w, orderBy: _ob, limit: _lim } = options;
305
+ const depKey = `${_ob || ""}-${_lim || 0}-${Object.entries(_w || {}).sort().join(",")}`;
306
+ (0, import_react.useEffect)(() => {
307
+ const query = collection.live(options);
308
+ let mounted = true;
309
+ const unsub = query.subscribe((result) => {
310
+ if (mounted) setData(result);
311
+ });
312
+ return () => {
313
+ unsub();
314
+ mounted = false;
315
+ };
316
+ }, [depKey]);
317
+ return data;
318
+ }
319
+ function useDoc(collection, docId) {
320
+ const [doc, setDoc] = (0, import_react.useState)();
321
+ (0, import_react.useEffect)(() => {
322
+ if (!docId) {
323
+ setDoc(null);
324
+ return;
325
+ }
326
+ let mounted = true;
327
+ const unsub = collection.findOne(docId).subscribe((result) => {
328
+ if (mounted) setDoc(result);
329
+ });
330
+ return () => {
331
+ unsub();
332
+ mounted = false;
333
+ };
334
+ }, [docId]);
335
+ return doc;
336
+ }
337
+ function useSync(client) {
338
+ const [state, setState] = (0, import_react.useState)({ connected: false, pendingCount: 0 });
339
+ (0, import_react.useEffect)(() => {
340
+ const interval = setInterval(() => {
341
+ setState({ connected: client.connectionState === "connected", pendingCount: 0 });
342
+ }, 1e3);
343
+ return () => clearInterval(interval);
344
+ }, []);
345
+ return state;
346
+ }
347
+ function usePresence(_client) {
348
+ return {};
349
+ }
350
+ function useMutation(collection) {
351
+ return {
352
+ put: (0, import_react.useCallback)((doc) => collection.put(doc), []),
353
+ update: (0, import_react.useCallback)((id, fields) => collection.update(id, fields), []),
354
+ remove: (0, import_react.useCallback)((id) => collection.delete(id), [])
355
+ };
356
+ }
357
+ function shallowEqual(a, b) {
358
+ const ka = Object.keys(a), kb = Object.keys(b);
359
+ if (ka.length !== kb.length) return false;
360
+ for (const k of ka) if (a[k] !== b[k]) return false;
361
+ return true;
362
+ }
363
+ function useQueryOptimized(query) {
364
+ const [docMap, setDocMap] = (0, import_react.useState)(/* @__PURE__ */ new Map());
365
+ (0, import_react.useEffect)(() => {
366
+ let mounted = true;
367
+ const unsub = query.subscribe((docs) => {
368
+ if (!mounted) return;
369
+ setDocMap((prev) => {
370
+ const next = new Map(prev);
371
+ const newIds = /* @__PURE__ */ new Set();
372
+ for (const doc of docs) {
373
+ const id = doc.id;
374
+ newIds.add(id);
375
+ const existing = prev.get(id);
376
+ if (!existing || !shallowEqual(existing, doc)) next.set(id, { ...doc });
377
+ }
378
+ for (const id of prev.keys()) {
379
+ if (!newIds.has(id)) next.delete(id);
380
+ }
381
+ if (next.size === prev.size) {
382
+ let same = true;
383
+ for (const [id, d] of next) {
384
+ if (prev.get(id) !== d) {
385
+ same = false;
386
+ break;
387
+ }
388
+ }
389
+ if (same) return prev;
390
+ }
391
+ return next;
392
+ });
393
+ });
394
+ return () => {
395
+ unsub();
396
+ mounted = false;
397
+ };
398
+ }, []);
399
+ return docMap;
400
+ }
401
+
402
+ // src/index.ts
403
+ var import_meridian_shared2 = require("meridian-shared");
404
+ // Annotate the CommonJS export names for ESM import in node:
405
+ 0 && (module.exports = {
406
+ createRNClient,
407
+ defineSchema,
408
+ useDoc,
409
+ useLiveQuery,
410
+ useMutation,
411
+ usePresence,
412
+ useQuery,
413
+ useQueryOptimized,
414
+ useSync,
415
+ z
416
+ });
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/rn-client.ts
9
+ import { HLC, generateNodeId } from "meridian-shared";
10
+ var AsyncKVStore = class {
11
+ memoryFallback = /* @__PURE__ */ new Map();
12
+ async get(key) {
13
+ try {
14
+ const AsyncStorage = __require("@react-native-async-storage/async-storage").default;
15
+ return await AsyncStorage.getItem(`meridian:${key}`);
16
+ } catch {
17
+ return this.memoryFallback.get(key) ?? null;
18
+ }
19
+ }
20
+ async set(key, value) {
21
+ try {
22
+ const AsyncStorage = __require("@react-native-async-storage/async-storage").default;
23
+ await AsyncStorage.setItem(`meridian:${key}`, value);
24
+ } catch {
25
+ this.memoryFallback.set(key, value);
26
+ }
27
+ }
28
+ async delete(key) {
29
+ try {
30
+ const AsyncStorage = __require("@react-native-async-storage/async-storage").default;
31
+ await AsyncStorage.removeItem(`meridian:${key}`);
32
+ } catch {
33
+ this.memoryFallback.delete(key);
34
+ }
35
+ }
36
+ async keys() {
37
+ try {
38
+ const AsyncStorage = __require("@react-native-async-storage/async-storage").default;
39
+ const allKeys = await AsyncStorage.getAllKeys();
40
+ return allKeys.filter((k) => k.startsWith("meridian:")).map((k) => k.replace("meridian:", ""));
41
+ } catch {
42
+ return Array.from(this.memoryFallback.keys());
43
+ }
44
+ }
45
+ };
46
+ function createRNClient(config) {
47
+ const { schema, serverUrl, auth, debug = false } = config;
48
+ const nodeId = generateNodeId();
49
+ const clock = new HLC(nodeId);
50
+ const store = new AsyncKVStore();
51
+ const collections = {};
52
+ let ws = null;
53
+ let state = "disconnected";
54
+ let destroyed = false;
55
+ for (const name of Object.keys(schema.collections)) {
56
+ const proxy = new RNCollectionProxy(name, store, clock, () => ws, serverUrl, auth, debug);
57
+ collections[name] = proxy;
58
+ }
59
+ connect().catch(() => {
60
+ });
61
+ async function connect() {
62
+ if (destroyed) return;
63
+ state = "connecting";
64
+ try {
65
+ ws = new WebSocket(serverUrl);
66
+ ws.onopen = async () => {
67
+ state = "connected";
68
+ if (auth) {
69
+ const token = await auth.getToken();
70
+ ws?.send(JSON.stringify({ type: "auth", token }));
71
+ }
72
+ };
73
+ ws.onmessage = (event) => {
74
+ try {
75
+ const msg = JSON.parse(event.data);
76
+ if (msg.type === "ack" && msg.opIds) {
77
+ for (const id of msg.opIds) {
78
+ store.delete(`pending:${id}`).catch(() => {
79
+ });
80
+ }
81
+ }
82
+ } catch {
83
+ }
84
+ };
85
+ ws.onerror = () => {
86
+ state = "error";
87
+ };
88
+ ws.onclose = () => {
89
+ state = "disconnected";
90
+ if (!destroyed) setTimeout(connect, 3e3);
91
+ };
92
+ } catch {
93
+ state = "disconnected";
94
+ if (!destroyed) setTimeout(connect, 5e3);
95
+ }
96
+ }
97
+ const client = {
98
+ get connectionState() {
99
+ return state;
100
+ },
101
+ async sync() {
102
+ if (ws && state === "connected") {
103
+ const pendingKeys = await store.keys();
104
+ const ops = [];
105
+ for (const key of pendingKeys) {
106
+ if (key.startsWith("pending:")) {
107
+ const opJson = await store.get(key);
108
+ if (opJson) ops.push(JSON.parse(opJson));
109
+ }
110
+ }
111
+ if (ops.length > 0) {
112
+ ws.send(JSON.stringify({ type: "push", ops }));
113
+ }
114
+ }
115
+ },
116
+ destroy() {
117
+ destroyed = true;
118
+ if (ws) {
119
+ ws.onclose = null;
120
+ ws.close();
121
+ ws = null;
122
+ }
123
+ }
124
+ };
125
+ for (const [name, proxy] of Object.entries(collections)) {
126
+ client[name] = proxy;
127
+ }
128
+ return client;
129
+ }
130
+ var RNCollectionProxy = class {
131
+ collection;
132
+ store;
133
+ clock;
134
+ getWs;
135
+ serverUrl;
136
+ auth;
137
+ debug;
138
+ constructor(collection, store, clock, getWs, serverUrl, auth, debug) {
139
+ this.collection = collection;
140
+ this.store = store;
141
+ this.clock = clock;
142
+ this.getWs = getWs;
143
+ this.serverUrl = serverUrl;
144
+ this.auth = auth;
145
+ this.debug = debug;
146
+ }
147
+ find(filter) {
148
+ const self = this;
149
+ const listeners = /* @__PURE__ */ new Set();
150
+ return {
151
+ subscribe(cb) {
152
+ listeners.add(cb);
153
+ self.queryDocs(filter).then(cb);
154
+ return () => {
155
+ listeners.delete(cb);
156
+ };
157
+ },
158
+ get: () => self.queryDocs(filter)
159
+ };
160
+ }
161
+ findOne(id) {
162
+ return {
163
+ subscribe: (cb) => {
164
+ this.getDoc(id).then(cb);
165
+ return () => {
166
+ };
167
+ },
168
+ get: () => this.getDoc(id)
169
+ };
170
+ }
171
+ live(options = {}) {
172
+ return this.find(options.where);
173
+ }
174
+ async put(doc) {
175
+ if (!doc.id) doc.id = `rn-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
176
+ const hlcTs = this.clock.now();
177
+ const hlcStr = `${hlcTs.wallTime}-${hlcTs.counter.toString().padStart(4, "0")}-${hlcTs.nodeId}`;
178
+ await this.store.set(`doc:${this.collection}:${doc.id}`, JSON.stringify(doc));
179
+ await this.queueOp(doc.id, Object.keys(doc).filter((k) => k !== "id"), doc, hlcStr);
180
+ this.trySync();
181
+ }
182
+ async update(id, fields) {
183
+ const existing = await this.getDoc(id);
184
+ if (!existing) return;
185
+ const merged = { ...existing, ...fields };
186
+ await this.store.set(`doc:${this.collection}:${id}`, JSON.stringify(merged));
187
+ const hlcTs = this.clock.now();
188
+ const hlcStr = `${hlcTs.wallTime}-${hlcTs.counter.toString().padStart(4, "0")}-${hlcTs.nodeId}`;
189
+ await this.queueOp(id, Object.keys(fields), fields, hlcStr);
190
+ this.trySync();
191
+ }
192
+ async delete(id) {
193
+ await this.store.set(`doc:${this.collection}:${id}:deleted`, "true");
194
+ this.trySync();
195
+ }
196
+ async queryDocs(filter) {
197
+ const allKeys = await this.store.keys();
198
+ const docs = [];
199
+ for (const key of allKeys) {
200
+ if (!key.startsWith(`doc:${this.collection}:`)) continue;
201
+ const id = key.split(":").pop();
202
+ const deleted = await this.store.get(`doc:${this.collection}:${id}:deleted`);
203
+ if (deleted === "true") continue;
204
+ const raw = await this.store.get(key);
205
+ if (!raw) continue;
206
+ const doc = JSON.parse(raw);
207
+ if (filter) {
208
+ let matches = true;
209
+ for (const [k, v] of Object.entries(filter)) {
210
+ if (doc[k] !== v) {
211
+ matches = false;
212
+ break;
213
+ }
214
+ }
215
+ if (!matches) continue;
216
+ }
217
+ docs.push(doc);
218
+ }
219
+ return docs;
220
+ }
221
+ async getDoc(id) {
222
+ const raw = await this.store.get(`doc:${this.collection}:${id}`);
223
+ if (!raw) return null;
224
+ return JSON.parse(raw);
225
+ }
226
+ async queueOp(docId, fields, values, hlc) {
227
+ for (const field of fields) {
228
+ const op = {
229
+ id: `${docId}-${field}-${hlc}`,
230
+ collection: this.collection,
231
+ docId,
232
+ field,
233
+ value: values[field],
234
+ hlc,
235
+ nodeId: this.clock.peek().nodeId
236
+ };
237
+ await this.store.set(`pending:${op.id}`, JSON.stringify(op));
238
+ }
239
+ }
240
+ trySync() {
241
+ const ws = this.getWs();
242
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
243
+ this.store.keys().then(async (keys) => {
244
+ const ops = [];
245
+ for (const key of keys) {
246
+ if (key.startsWith("pending:")) {
247
+ const raw = await this.store.get(key);
248
+ if (raw) ops.push(JSON.parse(raw));
249
+ }
250
+ }
251
+ if (ops.length > 0) {
252
+ ws.send(JSON.stringify({ type: "push", ops }));
253
+ }
254
+ });
255
+ }
256
+ };
257
+
258
+ // src/hooks.ts
259
+ import { useState, useEffect, useCallback } from "react";
260
+ function useQuery(query) {
261
+ const [data, setData] = useState();
262
+ useEffect(() => {
263
+ let mounted = true;
264
+ const unsub = query.subscribe((result) => {
265
+ if (mounted) setData(result);
266
+ });
267
+ return () => {
268
+ unsub();
269
+ mounted = false;
270
+ };
271
+ }, []);
272
+ return data;
273
+ }
274
+ function useLiveQuery(collection, options = {}) {
275
+ const [data, setData] = useState();
276
+ const { where: _w, orderBy: _ob, limit: _lim } = options;
277
+ const depKey = `${_ob || ""}-${_lim || 0}-${Object.entries(_w || {}).sort().join(",")}`;
278
+ useEffect(() => {
279
+ const query = collection.live(options);
280
+ let mounted = true;
281
+ const unsub = query.subscribe((result) => {
282
+ if (mounted) setData(result);
283
+ });
284
+ return () => {
285
+ unsub();
286
+ mounted = false;
287
+ };
288
+ }, [depKey]);
289
+ return data;
290
+ }
291
+ function useDoc(collection, docId) {
292
+ const [doc, setDoc] = useState();
293
+ useEffect(() => {
294
+ if (!docId) {
295
+ setDoc(null);
296
+ return;
297
+ }
298
+ let mounted = true;
299
+ const unsub = collection.findOne(docId).subscribe((result) => {
300
+ if (mounted) setDoc(result);
301
+ });
302
+ return () => {
303
+ unsub();
304
+ mounted = false;
305
+ };
306
+ }, [docId]);
307
+ return doc;
308
+ }
309
+ function useSync(client) {
310
+ const [state, setState] = useState({ connected: false, pendingCount: 0 });
311
+ useEffect(() => {
312
+ const interval = setInterval(() => {
313
+ setState({ connected: client.connectionState === "connected", pendingCount: 0 });
314
+ }, 1e3);
315
+ return () => clearInterval(interval);
316
+ }, []);
317
+ return state;
318
+ }
319
+ function usePresence(_client) {
320
+ return {};
321
+ }
322
+ function useMutation(collection) {
323
+ return {
324
+ put: useCallback((doc) => collection.put(doc), []),
325
+ update: useCallback((id, fields) => collection.update(id, fields), []),
326
+ remove: useCallback((id) => collection.delete(id), [])
327
+ };
328
+ }
329
+ function shallowEqual(a, b) {
330
+ const ka = Object.keys(a), kb = Object.keys(b);
331
+ if (ka.length !== kb.length) return false;
332
+ for (const k of ka) if (a[k] !== b[k]) return false;
333
+ return true;
334
+ }
335
+ function useQueryOptimized(query) {
336
+ const [docMap, setDocMap] = useState(/* @__PURE__ */ new Map());
337
+ useEffect(() => {
338
+ let mounted = true;
339
+ const unsub = query.subscribe((docs) => {
340
+ if (!mounted) return;
341
+ setDocMap((prev) => {
342
+ const next = new Map(prev);
343
+ const newIds = /* @__PURE__ */ new Set();
344
+ for (const doc of docs) {
345
+ const id = doc.id;
346
+ newIds.add(id);
347
+ const existing = prev.get(id);
348
+ if (!existing || !shallowEqual(existing, doc)) next.set(id, { ...doc });
349
+ }
350
+ for (const id of prev.keys()) {
351
+ if (!newIds.has(id)) next.delete(id);
352
+ }
353
+ if (next.size === prev.size) {
354
+ let same = true;
355
+ for (const [id, d] of next) {
356
+ if (prev.get(id) !== d) {
357
+ same = false;
358
+ break;
359
+ }
360
+ }
361
+ if (same) return prev;
362
+ }
363
+ return next;
364
+ });
365
+ });
366
+ return () => {
367
+ unsub();
368
+ mounted = false;
369
+ };
370
+ }, []);
371
+ return docMap;
372
+ }
373
+
374
+ // src/index.ts
375
+ import {
376
+ defineSchema,
377
+ z
378
+ } from "meridian-shared";
379
+ export {
380
+ createRNClient,
381
+ defineSchema,
382
+ useDoc,
383
+ useLiveQuery,
384
+ useMutation,
385
+ usePresence,
386
+ useQuery,
387
+ useQueryOptimized,
388
+ useSync,
389
+ z
390
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "meridian-react-native",
3
+ "version": "0.4.0",
4
+ "description": "React Native client for Meridian — local-first sync with SQLite/AsyncStorage",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format esm,cjs --external react --external react-native",
10
+ "test": "vitest run --passWithNoTests",
11
+ "dev": "tsup src/index.ts --format esm,cjs --watch --external react --external react-native"
12
+ },
13
+ "peerDependencies": {
14
+ "react": "^18.0.0 || ^19.0.0",
15
+ "react-native": "^0.72.0"
16
+ },
17
+ "dependencies": {
18
+ "meridian-client": "workspace:*",
19
+ "meridian-shared": "workspace:*",
20
+ "@react-native-async-storage/async-storage": "^1.21.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^19.0.0",
24
+ "react": "^19.0.0",
25
+ "react-native": "^0.76.0",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.5.0",
28
+ "vitest": "^2.0.0"
29
+ },
30
+ "files": ["dist"],
31
+ "license": "Apache-2.0",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/vahapogut/MeridianDB"
35
+ },
36
+ "keywords": ["react-native", "meridian", "sync", "local-first", "offline-first", "crdt", "mobile"],
37
+ "author": "Abdulvahap OGUT <info@ipeclabs.com>"
38
+ }