syncora 0.2.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/CHANGELOG.md +5 -0
- package/LICENSE +15 -0
- package/README.md +149 -0
- package/dist/chunk-VZXBPBHO.js +116 -0
- package/dist/chunk-VZXBPBHO.js.map +1 -0
- package/dist/client-OmEeE-w1.d.cts +136 -0
- package/dist/client-OmEeE-w1.d.ts +136 -0
- package/dist/index.cjs +591 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +457 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +105 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +19 -0
- package/dist/react.d.ts +19 -0
- package/dist/react.js +73 -0
- package/dist/react.js.map +1 -0
- package/package.json +87 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { d as SyncDocument, F as Filter, b as StoreSnapshot, C as ChangeEvent, a as ClientMessage } from './client-OmEeE-w1.cjs';
|
|
3
|
+
export { S as ServerMessage, c as Subscription, e as SyncoraClient, f as SyncoraClientOptions } from './client-OmEeE-w1.cjs';
|
|
4
|
+
|
|
5
|
+
interface SyncStore {
|
|
6
|
+
insert<T extends SyncDocument>(collection: string, document: Omit<T, "_id"> & {
|
|
7
|
+
_id?: string;
|
|
8
|
+
}): T;
|
|
9
|
+
update<T extends SyncDocument>(collection: string, id: string, patch: Partial<T>): T | undefined;
|
|
10
|
+
delete(collection: string, id: string): boolean;
|
|
11
|
+
find<T extends SyncDocument>(collection: string, filter?: Filter): T[];
|
|
12
|
+
findOne<T extends SyncDocument>(collection: string, id: string): T | undefined;
|
|
13
|
+
snapshot<T extends SyncDocument>(collection: string, filter?: Filter): StoreSnapshot<T>;
|
|
14
|
+
watch(handler: (event: ChangeEvent) => void): () => void;
|
|
15
|
+
version(collection: string): number;
|
|
16
|
+
reset(): void;
|
|
17
|
+
}
|
|
18
|
+
declare class MemoryStore implements SyncStore {
|
|
19
|
+
private collections;
|
|
20
|
+
private versions;
|
|
21
|
+
private watchers;
|
|
22
|
+
insert<T extends SyncDocument>(collection: string, doc: Omit<T, "_id"> & {
|
|
23
|
+
_id?: string;
|
|
24
|
+
}): T;
|
|
25
|
+
update<T extends SyncDocument>(collection: string, id: string, patch: Partial<T>): T | undefined;
|
|
26
|
+
delete(collection: string, id: string): boolean;
|
|
27
|
+
find<T extends SyncDocument>(collection: string, filter?: Filter): T[];
|
|
28
|
+
findOne<T extends SyncDocument>(collection: string, id: string): T | undefined;
|
|
29
|
+
snapshot<T extends SyncDocument>(collection: string, filter?: Filter): StoreSnapshot<T>;
|
|
30
|
+
watch(handler: (event: ChangeEvent) => void): () => void;
|
|
31
|
+
version(collection: string): number;
|
|
32
|
+
reset(): void;
|
|
33
|
+
private ensureCollection;
|
|
34
|
+
private bumpVersion;
|
|
35
|
+
private emit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SyncServerOptions {
|
|
39
|
+
store?: SyncStore;
|
|
40
|
+
server?: Server;
|
|
41
|
+
port?: number;
|
|
42
|
+
host?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
authorize?: (handshake: {
|
|
45
|
+
headers: Record<string, string | string[] | undefined>;
|
|
46
|
+
url?: string;
|
|
47
|
+
}) => boolean | Promise<boolean>;
|
|
48
|
+
permit?: (clientId: string, message: ClientMessage) => boolean | Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
declare class SyncServer {
|
|
51
|
+
readonly store: SyncStore;
|
|
52
|
+
private wss;
|
|
53
|
+
private clients;
|
|
54
|
+
private unwatch;
|
|
55
|
+
private serverVersion;
|
|
56
|
+
constructor(options?: SyncServerOptions);
|
|
57
|
+
address(): {
|
|
58
|
+
host: string;
|
|
59
|
+
port: number;
|
|
60
|
+
} | null;
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
attachChangeStream(emitter: {
|
|
63
|
+
on: (event: "change", handler: (event: ChangeEvent) => void) => unknown;
|
|
64
|
+
}): () => void;
|
|
65
|
+
private handleConnection;
|
|
66
|
+
private handleMessage;
|
|
67
|
+
private handleSubscribe;
|
|
68
|
+
private handleMutation;
|
|
69
|
+
private fanoutEvent;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare function applyEventToDocuments<T extends SyncDocument>(documents: T[], version: number, event: ChangeEvent, filter?: Filter): {
|
|
73
|
+
data: T[];
|
|
74
|
+
version: number;
|
|
75
|
+
};
|
|
76
|
+
declare function applyOptimisticInsert<T extends SyncDocument>(documents: T[], document: T): T[];
|
|
77
|
+
declare function applyOptimisticUpdate<T extends SyncDocument>(documents: T[], id: string, patch: Partial<T>): T[];
|
|
78
|
+
declare function applyOptimisticDelete<T extends SyncDocument>(documents: T[], id: string): T[];
|
|
79
|
+
|
|
80
|
+
declare function matches(doc: SyncDocument, filter: Filter | undefined): boolean;
|
|
81
|
+
|
|
82
|
+
export { ChangeEvent, ClientMessage, Filter, MemoryStore, StoreSnapshot, SyncDocument, SyncServer, type SyncServerOptions, type SyncStore, applyEventToDocuments, applyOptimisticDelete, applyOptimisticInsert, applyOptimisticUpdate, matches };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
import { d as SyncDocument, F as Filter, b as StoreSnapshot, C as ChangeEvent, a as ClientMessage } from './client-OmEeE-w1.js';
|
|
3
|
+
export { S as ServerMessage, c as Subscription, e as SyncoraClient, f as SyncoraClientOptions } from './client-OmEeE-w1.js';
|
|
4
|
+
|
|
5
|
+
interface SyncStore {
|
|
6
|
+
insert<T extends SyncDocument>(collection: string, document: Omit<T, "_id"> & {
|
|
7
|
+
_id?: string;
|
|
8
|
+
}): T;
|
|
9
|
+
update<T extends SyncDocument>(collection: string, id: string, patch: Partial<T>): T | undefined;
|
|
10
|
+
delete(collection: string, id: string): boolean;
|
|
11
|
+
find<T extends SyncDocument>(collection: string, filter?: Filter): T[];
|
|
12
|
+
findOne<T extends SyncDocument>(collection: string, id: string): T | undefined;
|
|
13
|
+
snapshot<T extends SyncDocument>(collection: string, filter?: Filter): StoreSnapshot<T>;
|
|
14
|
+
watch(handler: (event: ChangeEvent) => void): () => void;
|
|
15
|
+
version(collection: string): number;
|
|
16
|
+
reset(): void;
|
|
17
|
+
}
|
|
18
|
+
declare class MemoryStore implements SyncStore {
|
|
19
|
+
private collections;
|
|
20
|
+
private versions;
|
|
21
|
+
private watchers;
|
|
22
|
+
insert<T extends SyncDocument>(collection: string, doc: Omit<T, "_id"> & {
|
|
23
|
+
_id?: string;
|
|
24
|
+
}): T;
|
|
25
|
+
update<T extends SyncDocument>(collection: string, id: string, patch: Partial<T>): T | undefined;
|
|
26
|
+
delete(collection: string, id: string): boolean;
|
|
27
|
+
find<T extends SyncDocument>(collection: string, filter?: Filter): T[];
|
|
28
|
+
findOne<T extends SyncDocument>(collection: string, id: string): T | undefined;
|
|
29
|
+
snapshot<T extends SyncDocument>(collection: string, filter?: Filter): StoreSnapshot<T>;
|
|
30
|
+
watch(handler: (event: ChangeEvent) => void): () => void;
|
|
31
|
+
version(collection: string): number;
|
|
32
|
+
reset(): void;
|
|
33
|
+
private ensureCollection;
|
|
34
|
+
private bumpVersion;
|
|
35
|
+
private emit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SyncServerOptions {
|
|
39
|
+
store?: SyncStore;
|
|
40
|
+
server?: Server;
|
|
41
|
+
port?: number;
|
|
42
|
+
host?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
authorize?: (handshake: {
|
|
45
|
+
headers: Record<string, string | string[] | undefined>;
|
|
46
|
+
url?: string;
|
|
47
|
+
}) => boolean | Promise<boolean>;
|
|
48
|
+
permit?: (clientId: string, message: ClientMessage) => boolean | Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
declare class SyncServer {
|
|
51
|
+
readonly store: SyncStore;
|
|
52
|
+
private wss;
|
|
53
|
+
private clients;
|
|
54
|
+
private unwatch;
|
|
55
|
+
private serverVersion;
|
|
56
|
+
constructor(options?: SyncServerOptions);
|
|
57
|
+
address(): {
|
|
58
|
+
host: string;
|
|
59
|
+
port: number;
|
|
60
|
+
} | null;
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
attachChangeStream(emitter: {
|
|
63
|
+
on: (event: "change", handler: (event: ChangeEvent) => void) => unknown;
|
|
64
|
+
}): () => void;
|
|
65
|
+
private handleConnection;
|
|
66
|
+
private handleMessage;
|
|
67
|
+
private handleSubscribe;
|
|
68
|
+
private handleMutation;
|
|
69
|
+
private fanoutEvent;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare function applyEventToDocuments<T extends SyncDocument>(documents: T[], version: number, event: ChangeEvent, filter?: Filter): {
|
|
73
|
+
data: T[];
|
|
74
|
+
version: number;
|
|
75
|
+
};
|
|
76
|
+
declare function applyOptimisticInsert<T extends SyncDocument>(documents: T[], document: T): T[];
|
|
77
|
+
declare function applyOptimisticUpdate<T extends SyncDocument>(documents: T[], id: string, patch: Partial<T>): T[];
|
|
78
|
+
declare function applyOptimisticDelete<T extends SyncDocument>(documents: T[], id: string): T[];
|
|
79
|
+
|
|
80
|
+
declare function matches(doc: SyncDocument, filter: Filter | undefined): boolean;
|
|
81
|
+
|
|
82
|
+
export { ChangeEvent, ClientMessage, Filter, MemoryStore, StoreSnapshot, SyncDocument, SyncServer, type SyncServerOptions, type SyncStore, applyEventToDocuments, applyOptimisticDelete, applyOptimisticInsert, applyOptimisticUpdate, matches };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyEventToDocuments,
|
|
3
|
+
applyOptimisticDelete,
|
|
4
|
+
applyOptimisticInsert,
|
|
5
|
+
applyOptimisticUpdate,
|
|
6
|
+
matches
|
|
7
|
+
} from "./chunk-VZXBPBHO.js";
|
|
8
|
+
|
|
9
|
+
// src/server.ts
|
|
10
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
11
|
+
import { WebSocketServer } from "ws";
|
|
12
|
+
|
|
13
|
+
// src/store.ts
|
|
14
|
+
import { randomBytes } from "crypto";
|
|
15
|
+
var MemoryStore = class {
|
|
16
|
+
collections = /* @__PURE__ */ new Map();
|
|
17
|
+
versions = /* @__PURE__ */ new Map();
|
|
18
|
+
watchers = /* @__PURE__ */ new Set();
|
|
19
|
+
insert(collection, doc) {
|
|
20
|
+
const id = doc._id ?? `doc_${randomBytes(8).toString("hex")}`;
|
|
21
|
+
const map = this.ensureCollection(collection);
|
|
22
|
+
const document = { ...doc, _id: id };
|
|
23
|
+
map.set(id, document);
|
|
24
|
+
const version = this.bumpVersion(collection);
|
|
25
|
+
this.emit({ type: "insert", collection, document, version });
|
|
26
|
+
return document;
|
|
27
|
+
}
|
|
28
|
+
update(collection, id, patch) {
|
|
29
|
+
const map = this.collections.get(collection);
|
|
30
|
+
if (!map) return void 0;
|
|
31
|
+
const existing = map.get(id);
|
|
32
|
+
if (!existing) return void 0;
|
|
33
|
+
const updated = { ...existing, ...patch, _id: id };
|
|
34
|
+
map.set(id, updated);
|
|
35
|
+
const version = this.bumpVersion(collection);
|
|
36
|
+
this.emit({ type: "update", collection, documentId: id, patch, version });
|
|
37
|
+
return updated;
|
|
38
|
+
}
|
|
39
|
+
delete(collection, id) {
|
|
40
|
+
const map = this.collections.get(collection);
|
|
41
|
+
if (!map) return false;
|
|
42
|
+
if (!map.delete(id)) return false;
|
|
43
|
+
const version = this.bumpVersion(collection);
|
|
44
|
+
this.emit({ type: "delete", collection, documentId: id, version });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
find(collection, filter) {
|
|
48
|
+
const map = this.collections.get(collection);
|
|
49
|
+
if (!map) return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const doc of map.values()) {
|
|
52
|
+
if (matches(doc, filter)) out.push(doc);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
findOne(collection, id) {
|
|
57
|
+
return this.collections.get(collection)?.get(id);
|
|
58
|
+
}
|
|
59
|
+
snapshot(collection, filter) {
|
|
60
|
+
return { documents: this.find(collection, filter), version: this.version(collection) };
|
|
61
|
+
}
|
|
62
|
+
watch(handler) {
|
|
63
|
+
this.watchers.add(handler);
|
|
64
|
+
return () => this.watchers.delete(handler);
|
|
65
|
+
}
|
|
66
|
+
version(collection) {
|
|
67
|
+
return this.versions.get(collection) ?? 0;
|
|
68
|
+
}
|
|
69
|
+
reset() {
|
|
70
|
+
this.collections.clear();
|
|
71
|
+
this.versions.clear();
|
|
72
|
+
}
|
|
73
|
+
ensureCollection(collection) {
|
|
74
|
+
let map = this.collections.get(collection);
|
|
75
|
+
if (!map) {
|
|
76
|
+
map = /* @__PURE__ */ new Map();
|
|
77
|
+
this.collections.set(collection, map);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
81
|
+
bumpVersion(collection) {
|
|
82
|
+
const next = this.version(collection) + 1;
|
|
83
|
+
this.versions.set(collection, next);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
emit(event) {
|
|
87
|
+
for (const w of this.watchers) {
|
|
88
|
+
try {
|
|
89
|
+
w(event);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/server.ts
|
|
97
|
+
var SyncServer = class {
|
|
98
|
+
store;
|
|
99
|
+
wss;
|
|
100
|
+
clients = /* @__PURE__ */ new Map();
|
|
101
|
+
unwatch;
|
|
102
|
+
serverVersion = 0;
|
|
103
|
+
constructor(options = {}) {
|
|
104
|
+
this.store = options.store ?? new MemoryStore();
|
|
105
|
+
const wssConfig = options.server ? { server: options.server, path: options.path } : { host: options.host ?? "127.0.0.1", port: options.port ?? 0, path: options.path };
|
|
106
|
+
this.wss = new WebSocketServer(wssConfig);
|
|
107
|
+
this.wss.on("connection", (socket, req) => this.handleConnection(socket, req, options.authorize, options.permit));
|
|
108
|
+
this.unwatch = this.store.watch((event) => this.fanoutEvent(event));
|
|
109
|
+
}
|
|
110
|
+
address() {
|
|
111
|
+
const a = this.wss.address();
|
|
112
|
+
if (typeof a === "string" || a === null) return null;
|
|
113
|
+
return { host: a.address, port: a.port };
|
|
114
|
+
}
|
|
115
|
+
async close() {
|
|
116
|
+
this.unwatch();
|
|
117
|
+
for (const session of this.clients.values()) {
|
|
118
|
+
try {
|
|
119
|
+
session.socket.close();
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.clients.clear();
|
|
124
|
+
return new Promise((resolve) => this.wss.close(() => resolve()));
|
|
125
|
+
}
|
|
126
|
+
attachChangeStream(emitter) {
|
|
127
|
+
const handler = (event) => this.fanoutEvent(event);
|
|
128
|
+
emitter.on("change", handler);
|
|
129
|
+
return () => {
|
|
130
|
+
const off = emitter.off;
|
|
131
|
+
if (typeof off === "function") off.call(emitter, "change", handler);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async handleConnection(socket, req, authorize, permit) {
|
|
135
|
+
if (authorize) {
|
|
136
|
+
try {
|
|
137
|
+
const ok = await authorize({ headers: req.headers, url: req.url });
|
|
138
|
+
if (!ok) {
|
|
139
|
+
socket.close(4401, "unauthorized");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
socket.close(4401, "unauthorized");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const session = {
|
|
148
|
+
id: `c_${randomBytes2(8).toString("hex")}`,
|
|
149
|
+
socket,
|
|
150
|
+
subscriptions: /* @__PURE__ */ new Map()
|
|
151
|
+
};
|
|
152
|
+
this.clients.set(session.id, session);
|
|
153
|
+
socket.on("close", () => this.clients.delete(session.id));
|
|
154
|
+
socket.on("error", () => {
|
|
155
|
+
});
|
|
156
|
+
socket.on("message", (data) => {
|
|
157
|
+
this.handleMessage(session, data.toString(), permit).catch(() => {
|
|
158
|
+
send(socket, { type: "error", message: "internal error" });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
send(socket, { type: "hello", clientId: session.id, serverVersion: this.serverVersion });
|
|
162
|
+
}
|
|
163
|
+
async handleMessage(session, raw, permit) {
|
|
164
|
+
let message;
|
|
165
|
+
try {
|
|
166
|
+
message = JSON.parse(raw);
|
|
167
|
+
} catch {
|
|
168
|
+
send(session.socket, { type: "error", message: "invalid JSON" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (permit) {
|
|
172
|
+
try {
|
|
173
|
+
const allowed = await permit(session.id, message);
|
|
174
|
+
if (!allowed) {
|
|
175
|
+
send(session.socket, { type: "error", message: "forbidden" });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
send(session.socket, { type: "error", message: "permission check failed" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
switch (message.type) {
|
|
184
|
+
case "subscribe":
|
|
185
|
+
return this.handleSubscribe(session, message);
|
|
186
|
+
case "unsubscribe":
|
|
187
|
+
session.subscriptions.delete(message.subscriptionId);
|
|
188
|
+
return;
|
|
189
|
+
case "mutation":
|
|
190
|
+
return this.handleMutation(session, message);
|
|
191
|
+
default:
|
|
192
|
+
send(session.socket, { type: "error", message: "unknown message type" });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
handleSubscribe(session, message) {
|
|
196
|
+
session.subscriptions.set(message.subscriptionId, { collection: message.collection, filter: message.filter });
|
|
197
|
+
const snapshot = this.store.snapshot(message.collection, message.filter);
|
|
198
|
+
send(session.socket, {
|
|
199
|
+
type: "event",
|
|
200
|
+
subscriptionId: message.subscriptionId,
|
|
201
|
+
event: { type: "snapshot", collection: message.collection, documents: snapshot.documents, version: snapshot.version }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
handleMutation(session, message) {
|
|
205
|
+
try {
|
|
206
|
+
if (message.op === "insert") {
|
|
207
|
+
if (!message.document) throw new Error("document required");
|
|
208
|
+
this.store.insert(message.collection, message.document);
|
|
209
|
+
} else if (message.op === "update") {
|
|
210
|
+
if (!message.documentId || !message.patch) throw new Error("documentId and patch required");
|
|
211
|
+
const result = this.store.update(message.collection, message.documentId, message.patch);
|
|
212
|
+
if (!result) throw new Error("not found");
|
|
213
|
+
} else if (message.op === "delete") {
|
|
214
|
+
if (!message.documentId) throw new Error("documentId required");
|
|
215
|
+
const ok = this.store.delete(message.collection, message.documentId);
|
|
216
|
+
if (!ok) throw new Error("not found");
|
|
217
|
+
}
|
|
218
|
+
send(session.socket, { type: "ack", mutationId: message.mutationId, ok: true });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
send(session.socket, {
|
|
221
|
+
type: "ack",
|
|
222
|
+
mutationId: message.mutationId,
|
|
223
|
+
ok: false,
|
|
224
|
+
error: err instanceof Error ? err.message : String(err)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
fanoutEvent(event) {
|
|
229
|
+
this.serverVersion = Math.max(this.serverVersion, event.version);
|
|
230
|
+
for (const session of this.clients.values()) {
|
|
231
|
+
for (const [subscriptionId, sub] of session.subscriptions) {
|
|
232
|
+
if (sub.collection !== event.collection) continue;
|
|
233
|
+
if (!eventMatchesFilter(event, sub.filter)) continue;
|
|
234
|
+
send(session.socket, { type: "event", subscriptionId, event });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
function eventMatchesFilter(event, filter) {
|
|
240
|
+
if (!filter) return true;
|
|
241
|
+
if (event.type === "snapshot") return true;
|
|
242
|
+
if (event.type === "delete") return true;
|
|
243
|
+
if (event.type === "insert") return matches(event.document, filter);
|
|
244
|
+
if (event.type === "update") return matches(event.patch, filter);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
function send(socket, message) {
|
|
248
|
+
if (socket.readyState !== 1) return;
|
|
249
|
+
try {
|
|
250
|
+
socket.send(JSON.stringify(message));
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/client.ts
|
|
256
|
+
var SyncoraClient = class {
|
|
257
|
+
url;
|
|
258
|
+
WS;
|
|
259
|
+
socket;
|
|
260
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
261
|
+
pendingMutations = /* @__PURE__ */ new Map();
|
|
262
|
+
reconnectDelay;
|
|
263
|
+
maxReconnectDelay;
|
|
264
|
+
currentDelay;
|
|
265
|
+
intentionalClose = false;
|
|
266
|
+
clientId;
|
|
267
|
+
outbox = [];
|
|
268
|
+
connectedListeners = /* @__PURE__ */ new Set();
|
|
269
|
+
disconnectedListeners = /* @__PURE__ */ new Set();
|
|
270
|
+
onError;
|
|
271
|
+
constructor(options) {
|
|
272
|
+
this.url = options.url;
|
|
273
|
+
this.WS = options.WebSocket ?? globalThis.WebSocket;
|
|
274
|
+
if (!this.WS) throw new Error("No WebSocket implementation. Pass options.WebSocket explicitly.");
|
|
275
|
+
this.reconnectDelay = options.reconnectDelayMs ?? 500;
|
|
276
|
+
this.maxReconnectDelay = options.maxReconnectDelayMs ?? 15e3;
|
|
277
|
+
this.currentDelay = this.reconnectDelay;
|
|
278
|
+
this.onError = options.onError;
|
|
279
|
+
if (options.onConnect) this.connectedListeners.add(options.onConnect);
|
|
280
|
+
if (options.onDisconnect) this.disconnectedListeners.add(options.onDisconnect);
|
|
281
|
+
this.connect();
|
|
282
|
+
}
|
|
283
|
+
isConnected() {
|
|
284
|
+
return this.socket?.readyState === 1;
|
|
285
|
+
}
|
|
286
|
+
close() {
|
|
287
|
+
this.intentionalClose = true;
|
|
288
|
+
this.socket?.close();
|
|
289
|
+
}
|
|
290
|
+
onConnect(handler) {
|
|
291
|
+
this.connectedListeners.add(handler);
|
|
292
|
+
return () => this.connectedListeners.delete(handler);
|
|
293
|
+
}
|
|
294
|
+
onDisconnect(handler) {
|
|
295
|
+
this.disconnectedListeners.add(handler);
|
|
296
|
+
return () => this.disconnectedListeners.delete(handler);
|
|
297
|
+
}
|
|
298
|
+
subscribe(collection, options = {}) {
|
|
299
|
+
const id = `sub_${cryptoRandom()}`;
|
|
300
|
+
const sub = {
|
|
301
|
+
id,
|
|
302
|
+
collection,
|
|
303
|
+
filter: options.filter,
|
|
304
|
+
data: [],
|
|
305
|
+
version: 0,
|
|
306
|
+
listeners: /* @__PURE__ */ new Set()
|
|
307
|
+
};
|
|
308
|
+
this.subscriptions.set(id, sub);
|
|
309
|
+
this.send({ type: "subscribe", subscriptionId: id, collection, filter: options.filter });
|
|
310
|
+
const api = {
|
|
311
|
+
id,
|
|
312
|
+
collection,
|
|
313
|
+
filter: options.filter,
|
|
314
|
+
get data() {
|
|
315
|
+
return sub.data;
|
|
316
|
+
},
|
|
317
|
+
get version() {
|
|
318
|
+
return sub.version;
|
|
319
|
+
},
|
|
320
|
+
unsubscribe: () => {
|
|
321
|
+
this.subscriptions.delete(id);
|
|
322
|
+
this.send({ type: "unsubscribe", subscriptionId: id });
|
|
323
|
+
},
|
|
324
|
+
onChange: (listener) => {
|
|
325
|
+
const bridge = (state) => {
|
|
326
|
+
listener({ data: state.data, version: state.version });
|
|
327
|
+
};
|
|
328
|
+
sub.listeners.add(bridge);
|
|
329
|
+
bridge({ data: sub.data, version: sub.version });
|
|
330
|
+
return () => sub.listeners.delete(bridge);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
return api;
|
|
334
|
+
}
|
|
335
|
+
async mutate(collection, op, payload = {}) {
|
|
336
|
+
const mutationId = `mut_${cryptoRandom()}`;
|
|
337
|
+
const message = {
|
|
338
|
+
type: "mutation",
|
|
339
|
+
mutationId,
|
|
340
|
+
collection,
|
|
341
|
+
op,
|
|
342
|
+
...payload
|
|
343
|
+
};
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
this.pendingMutations.set(mutationId, { resolve, reject });
|
|
346
|
+
this.send(message);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
connect() {
|
|
350
|
+
this.intentionalClose = false;
|
|
351
|
+
let socket;
|
|
352
|
+
try {
|
|
353
|
+
socket = new this.WS(this.url);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
this.onError?.(err);
|
|
356
|
+
this.scheduleReconnect();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.socket = socket;
|
|
360
|
+
socket.addEventListener("open", () => {
|
|
361
|
+
this.currentDelay = this.reconnectDelay;
|
|
362
|
+
for (const msg of this.outbox) {
|
|
363
|
+
try {
|
|
364
|
+
socket.send(JSON.stringify(msg));
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this.outbox = [];
|
|
369
|
+
for (const sub of this.subscriptions.values()) {
|
|
370
|
+
try {
|
|
371
|
+
socket.send(JSON.stringify({ type: "subscribe", subscriptionId: sub.id, collection: sub.collection, filter: sub.filter }));
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
socket.addEventListener("message", (event) => {
|
|
377
|
+
let parsed;
|
|
378
|
+
try {
|
|
379
|
+
parsed = JSON.parse(typeof event.data === "string" ? event.data : String(event.data));
|
|
380
|
+
} catch {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.handleMessage(parsed);
|
|
384
|
+
});
|
|
385
|
+
socket.addEventListener("error", (err) => this.onError?.(err));
|
|
386
|
+
socket.addEventListener("close", () => {
|
|
387
|
+
for (const listener of this.disconnectedListeners) listener();
|
|
388
|
+
if (!this.intentionalClose) this.scheduleReconnect();
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
scheduleReconnect() {
|
|
392
|
+
const delay = this.currentDelay;
|
|
393
|
+
this.currentDelay = Math.min(this.maxReconnectDelay, this.currentDelay * 2);
|
|
394
|
+
setTimeout(() => this.connect(), delay);
|
|
395
|
+
}
|
|
396
|
+
handleMessage(message) {
|
|
397
|
+
switch (message.type) {
|
|
398
|
+
case "hello":
|
|
399
|
+
this.clientId = message.clientId;
|
|
400
|
+
for (const listener of this.connectedListeners) listener(message.clientId);
|
|
401
|
+
break;
|
|
402
|
+
case "event": {
|
|
403
|
+
const sub = this.subscriptions.get(message.subscriptionId);
|
|
404
|
+
if (!sub) return;
|
|
405
|
+
applyEventToSubscription(sub, message.event);
|
|
406
|
+
for (const l of sub.listeners) l({ data: sub.data, version: sub.version });
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case "ack": {
|
|
410
|
+
const pending = this.pendingMutations.get(message.mutationId);
|
|
411
|
+
if (!pending) return;
|
|
412
|
+
this.pendingMutations.delete(message.mutationId);
|
|
413
|
+
if (message.ok) pending.resolve();
|
|
414
|
+
else pending.reject(new Error(message.error ?? "mutation rejected"));
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "error":
|
|
418
|
+
this.onError?.(new Error(message.message));
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
send(message) {
|
|
423
|
+
if (this.socket && this.socket.readyState === 1) {
|
|
424
|
+
try {
|
|
425
|
+
this.socket.send(JSON.stringify(message));
|
|
426
|
+
return;
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
this.outbox.push(message);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
function applyEventToSubscription(sub, event) {
|
|
434
|
+
const { data, version } = applyEventToDocuments(sub.data, sub.version, event, sub.filter);
|
|
435
|
+
sub.data = data;
|
|
436
|
+
sub.version = version;
|
|
437
|
+
}
|
|
438
|
+
function cryptoRandom() {
|
|
439
|
+
const bytes = new Uint8Array(8);
|
|
440
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.getRandomValues === "function") {
|
|
441
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
442
|
+
} else {
|
|
443
|
+
for (let i = 0; i < 8; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
444
|
+
}
|
|
445
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
446
|
+
}
|
|
447
|
+
export {
|
|
448
|
+
MemoryStore,
|
|
449
|
+
SyncServer,
|
|
450
|
+
SyncoraClient,
|
|
451
|
+
applyEventToDocuments,
|
|
452
|
+
applyOptimisticDelete,
|
|
453
|
+
applyOptimisticInsert,
|
|
454
|
+
applyOptimisticUpdate,
|
|
455
|
+
matches
|
|
456
|
+
};
|
|
457
|
+
//# sourceMappingURL=index.js.map
|