@sveltebase/sync 1.0.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/README.md +480 -0
- package/dist/client/index.d.ts +47 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +326 -0
- package/dist/client/live.svelte.d.ts +18 -0
- package/dist/client/live.svelte.d.ts.map +1 -0
- package/dist/client/live.svelte.js +40 -0
- package/dist/global.d.ts +25 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/protocol.d.ts +41 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +14 -0
- package/dist/server/broker.d.ts +27 -0
- package/dist/server/broker.d.ts.map +1 -0
- package/dist/server/broker.js +230 -0
- package/dist/server/dev-engine.d.ts +10 -0
- package/dist/server/dev-engine.d.ts.map +1 -0
- package/dist/server/dev-engine.js +117 -0
- package/dist/server/engine.d.ts +13 -0
- package/dist/server/engine.d.ts.map +1 -0
- package/dist/server/engine.js +117 -0
- package/dist/server/handler.d.ts +3 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/handler.js +51 -0
- package/dist/server/index.d.ts +25 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +10 -0
- package/dist/vite.d.ts +6 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +55 -0
- package/package.json +49 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { parseSyncMessage } from "../protocol.js";
|
|
2
|
+
export class SyncBroker {
|
|
3
|
+
handlers;
|
|
4
|
+
connections = new Set();
|
|
5
|
+
authorizeConnection;
|
|
6
|
+
constructor(handlers, authorizeConnection) {
|
|
7
|
+
this.handlers = new Map();
|
|
8
|
+
this.setHandlers(handlers);
|
|
9
|
+
this.authorizeConnection = authorizeConnection;
|
|
10
|
+
}
|
|
11
|
+
setHandlers(handlers) {
|
|
12
|
+
this.handlers.clear();
|
|
13
|
+
for (const h of handlers) {
|
|
14
|
+
// Resolve static channel to register, or we can register dynamic channel routing
|
|
15
|
+
// If it's a dynamic channel resolver function, we will match channel prefix or resolve dynamically
|
|
16
|
+
// Let's support both static channels and dynamic channel prefixes (or lookups).
|
|
17
|
+
// If config.channel is a function, we register it under a wildcard or handle it dynamically.
|
|
18
|
+
if (typeof h.config.channel === "string") {
|
|
19
|
+
this.handlers.set(h.config.channel, h);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
registerConnection(conn) {
|
|
24
|
+
this.connections.add(conn);
|
|
25
|
+
}
|
|
26
|
+
removeConnection(conn) {
|
|
27
|
+
this.connections.delete(conn);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the appropriate handler for a channel name.
|
|
31
|
+
*/
|
|
32
|
+
findHandler(channel) {
|
|
33
|
+
// Static match
|
|
34
|
+
const handler = this.handlers.get(channel);
|
|
35
|
+
if (handler)
|
|
36
|
+
return handler;
|
|
37
|
+
// Dynamic match: check dynamic handlers
|
|
38
|
+
for (const h of this.handlers.values()) {
|
|
39
|
+
if (typeof h.config.channel === "function") {
|
|
40
|
+
// We'll check if it matches in some way, but generally dynamic channel is resolved
|
|
41
|
+
// during subscribe and stored.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Fallback: search all handlers to see if they resolve to this channel for a generic context
|
|
45
|
+
// (this is rare, usually channels are static or follow a pattern like channel:userId)
|
|
46
|
+
for (const [, h] of this.handlers) {
|
|
47
|
+
if (typeof h.config.channel === "function") {
|
|
48
|
+
// Let's assume dynamic channels are in format "prefix:id". We can match by prefix.
|
|
49
|
+
const staticChannelPrefix = typeof h.config.channel === "string" ? h.config.channel : "";
|
|
50
|
+
if (staticChannelPrefix && channel.startsWith(staticChannelPrefix)) {
|
|
51
|
+
return h;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Let's support matching prefix like "todos:" -> match the todos handler
|
|
56
|
+
const colonIndex = channel.indexOf(":");
|
|
57
|
+
if (colonIndex !== -1) {
|
|
58
|
+
const prefix = channel.substring(0, colonIndex);
|
|
59
|
+
const prefixHandler = this.handlers.get(prefix);
|
|
60
|
+
if (prefixHandler)
|
|
61
|
+
return prefixHandler;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
async handleMessage(conn, rawMessage, platform, request) {
|
|
66
|
+
const msg = parseSyncMessage(rawMessage);
|
|
67
|
+
if (!msg)
|
|
68
|
+
return;
|
|
69
|
+
const auth = conn.getAuth();
|
|
70
|
+
const ctx = {
|
|
71
|
+
platform,
|
|
72
|
+
request,
|
|
73
|
+
auth,
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
switch (msg.type) {
|
|
77
|
+
case "ping":
|
|
78
|
+
conn.send("pong");
|
|
79
|
+
break;
|
|
80
|
+
case "subscribe": {
|
|
81
|
+
const handler = this.findHandler(msg.channel);
|
|
82
|
+
if (!handler) {
|
|
83
|
+
conn.send(JSON.stringify({
|
|
84
|
+
type: "reject",
|
|
85
|
+
id: "subscribe",
|
|
86
|
+
error: `No handler registered for channel: ${msg.channel}`,
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Channel authorize
|
|
91
|
+
if (handler.config.authorize) {
|
|
92
|
+
await handler.config.authorize(ctx);
|
|
93
|
+
}
|
|
94
|
+
conn.getSubscribedChannels().add(msg.channel);
|
|
95
|
+
// Fetch snapshot with delta support
|
|
96
|
+
const data = await handler.config.fetch(ctx, msg.since);
|
|
97
|
+
conn.send(JSON.stringify({
|
|
98
|
+
type: "snapshot",
|
|
99
|
+
channel: msg.channel,
|
|
100
|
+
data,
|
|
101
|
+
isDelta: !!msg.since,
|
|
102
|
+
}));
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "unsubscribe":
|
|
106
|
+
conn.getSubscribedChannels().delete(msg.channel);
|
|
107
|
+
break;
|
|
108
|
+
case "mutate": {
|
|
109
|
+
const handler = this.findHandler(msg.channel);
|
|
110
|
+
if (!handler) {
|
|
111
|
+
conn.send(JSON.stringify({
|
|
112
|
+
type: "reject",
|
|
113
|
+
id: msg.id,
|
|
114
|
+
error: `No handler for channel: ${msg.channel}`,
|
|
115
|
+
}));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Authorize mutation
|
|
119
|
+
if (handler.config.authorize) {
|
|
120
|
+
await handler.config.authorize(ctx);
|
|
121
|
+
}
|
|
122
|
+
let result;
|
|
123
|
+
if (msg.action === "create") {
|
|
124
|
+
if (handler.config.validate?.create) {
|
|
125
|
+
msg.data = handler.config.validate.create.parse(msg.data);
|
|
126
|
+
}
|
|
127
|
+
if (!handler.config.create) {
|
|
128
|
+
throw new Error(`Forbidden: Create operation not supported on channel ${msg.channel}`);
|
|
129
|
+
}
|
|
130
|
+
result = await handler.config.create(ctx, msg.data);
|
|
131
|
+
}
|
|
132
|
+
else if (msg.action === "update") {
|
|
133
|
+
if (handler.config.validate?.update) {
|
|
134
|
+
msg.data = handler.config.validate.update.parse(msg.data);
|
|
135
|
+
}
|
|
136
|
+
if (!handler.config.update) {
|
|
137
|
+
throw new Error(`Forbidden: Update operation not supported on channel ${msg.channel}`);
|
|
138
|
+
}
|
|
139
|
+
result = await handler.config.update(ctx, msg.key, msg.data);
|
|
140
|
+
}
|
|
141
|
+
else if (msg.action === "delete") {
|
|
142
|
+
if (!handler.config.delete) {
|
|
143
|
+
throw new Error(`Forbidden: Delete operation not supported on channel ${msg.channel}`);
|
|
144
|
+
}
|
|
145
|
+
await handler.config.delete(ctx, msg.key);
|
|
146
|
+
result = { id: msg.key };
|
|
147
|
+
}
|
|
148
|
+
// Send Ack back to sender
|
|
149
|
+
conn.send(JSON.stringify({
|
|
150
|
+
type: "ack",
|
|
151
|
+
id: msg.id,
|
|
152
|
+
data: result,
|
|
153
|
+
}));
|
|
154
|
+
// Broadcast changes to other subscribers
|
|
155
|
+
this.broadcastChange(conn, msg.channel, msg.action, msg.key || result?.id, result, msg.id, handler, ctx);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
console.error(`SyncBroker: error handling message type=${msg.type}:`, err);
|
|
162
|
+
if (msg.type === "mutate") {
|
|
163
|
+
conn.send(JSON.stringify({
|
|
164
|
+
type: "reject",
|
|
165
|
+
id: msg.id,
|
|
166
|
+
error: err.message || "Server error",
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async broadcastChange(sender, channel, action, key, data, mutationId, handler, ctx) {
|
|
172
|
+
const changeMsg = JSON.stringify({
|
|
173
|
+
type: "change",
|
|
174
|
+
channel,
|
|
175
|
+
action,
|
|
176
|
+
key,
|
|
177
|
+
data,
|
|
178
|
+
mutationId,
|
|
179
|
+
});
|
|
180
|
+
// Determine scope
|
|
181
|
+
let allowedUserIds = "all";
|
|
182
|
+
if (handler.config.scope) {
|
|
183
|
+
try {
|
|
184
|
+
allowedUserIds = await handler.config.scope(ctx, action, data);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
console.error("SyncBroker: error resolving broadcast scope:", e);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const conn of this.connections) {
|
|
191
|
+
// Don't send to connections not subscribed to this channel
|
|
192
|
+
if (!conn.getSubscribedChannels().has(channel)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Filter based on scope
|
|
196
|
+
if (allowedUserIds !== "all") {
|
|
197
|
+
const connAuth = conn.getAuth();
|
|
198
|
+
const userId = connAuth?.userId;
|
|
199
|
+
if (!userId || !allowedUserIds.includes(userId)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
conn.send(changeMsg);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
this.connections.delete(conn);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async handleExternalChange(channel, action, key, data) {
|
|
212
|
+
const changeMsg = JSON.stringify({
|
|
213
|
+
type: "change",
|
|
214
|
+
channel,
|
|
215
|
+
action,
|
|
216
|
+
key,
|
|
217
|
+
data,
|
|
218
|
+
});
|
|
219
|
+
for (const conn of this.connections) {
|
|
220
|
+
if (conn.getSubscribedChannels().has(channel)) {
|
|
221
|
+
try {
|
|
222
|
+
conn.send(changeMsg);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
this.connections.delete(conn);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SyncHandler } from "./index.js";
|
|
2
|
+
import type { IncomingMessage } from "node:http";
|
|
3
|
+
export declare function setHandlers(handlers: SyncHandler[]): void;
|
|
4
|
+
export declare function addClient(ws: {
|
|
5
|
+
send: (data: string) => void;
|
|
6
|
+
close: (code?: number, reason?: string) => void;
|
|
7
|
+
on: (event: string, listener: (...args: any[]) => void) => void;
|
|
8
|
+
}, req: IncomingMessage): void;
|
|
9
|
+
export declare function broadcastExternalChange(channel: string, action: "create" | "update" | "delete", key: string | undefined, data: any): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=dev-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-engine.d.ts","sourceRoot":"","sources":["../../src/server/dev-engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAUjD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QASlD;AAcD,wBAAgB,SAAS,CACvB,EAAE,EAAE;IACF,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAC;CACjE,EACD,GAAG,EAAE,eAAe,QA+ErB;AAyBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG,iBAIV"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { SyncBroker } from "./broker.js";
|
|
2
|
+
const GLOBAL_KEY = "__sync_dev_broker__";
|
|
3
|
+
let devBroker = null;
|
|
4
|
+
export function setHandlers(handlers) {
|
|
5
|
+
const g = globalThis;
|
|
6
|
+
if (!g[GLOBAL_KEY]) {
|
|
7
|
+
const broker = new SyncBroker(handlers);
|
|
8
|
+
g[GLOBAL_KEY] = { broker };
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
g[GLOBAL_KEY].broker.setHandlers(handlers);
|
|
12
|
+
}
|
|
13
|
+
devBroker = g[GLOBAL_KEY].broker;
|
|
14
|
+
}
|
|
15
|
+
function getDevBroker() {
|
|
16
|
+
if (devBroker)
|
|
17
|
+
return devBroker;
|
|
18
|
+
const g = globalThis;
|
|
19
|
+
if (g[GLOBAL_KEY]) {
|
|
20
|
+
devBroker = g[GLOBAL_KEY].broker;
|
|
21
|
+
return devBroker;
|
|
22
|
+
}
|
|
23
|
+
throw new Error("Sync dev broker not initialized. Call setHandlers first.");
|
|
24
|
+
}
|
|
25
|
+
export function addClient(ws, req) {
|
|
26
|
+
const broker = getDevBroker();
|
|
27
|
+
const subscribedChannels = new Set();
|
|
28
|
+
let auth = null;
|
|
29
|
+
// Convert Node IncomingMessage headers to web-standard Headers
|
|
30
|
+
const headers = new Headers();
|
|
31
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
for (const v of value) {
|
|
36
|
+
headers.append(key, v);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
headers.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const urlObj = new URL(req.url ?? "", `http://${req.headers.host || "localhost"}`);
|
|
44
|
+
// Align with DO behavior to set local dev auth
|
|
45
|
+
const userId = urlObj.searchParams.get("userId") || headers.get("x-user-id");
|
|
46
|
+
if (userId) {
|
|
47
|
+
auth = { userId };
|
|
48
|
+
}
|
|
49
|
+
const conn = {
|
|
50
|
+
send(data) {
|
|
51
|
+
ws.send(data);
|
|
52
|
+
},
|
|
53
|
+
close(code, reason) {
|
|
54
|
+
ws.close(code, reason);
|
|
55
|
+
},
|
|
56
|
+
getAuth() {
|
|
57
|
+
return auth;
|
|
58
|
+
},
|
|
59
|
+
setAuth(newAuth) {
|
|
60
|
+
auth = newAuth;
|
|
61
|
+
},
|
|
62
|
+
getSubscribedChannels() {
|
|
63
|
+
return subscribedChannels;
|
|
64
|
+
},
|
|
65
|
+
headers,
|
|
66
|
+
url: urlObj.toString(),
|
|
67
|
+
};
|
|
68
|
+
broker.registerConnection(conn);
|
|
69
|
+
console.log("dev-engine: addClient registered connection");
|
|
70
|
+
ws.on("message", async (data) => {
|
|
71
|
+
const messageString = String(data);
|
|
72
|
+
console.log("dev-engine: WebSocket message received:", messageString.slice(0, 100));
|
|
73
|
+
const request = new Request(conn.url, {
|
|
74
|
+
headers: conn.headers,
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
console.log("dev-engine: getting platform proxy...");
|
|
78
|
+
const platform = await getPlatform();
|
|
79
|
+
console.log("dev-engine: platform proxy obtained, handling message...");
|
|
80
|
+
await broker.handleMessage(conn, messageString, platform, request);
|
|
81
|
+
console.log("dev-engine: message handled successfully");
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error("dev-engine: Error handling message:", err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
ws.on("close", () => {
|
|
88
|
+
console.log("dev-engine: WebSocket connection closed");
|
|
89
|
+
broker.removeConnection(conn);
|
|
90
|
+
});
|
|
91
|
+
ws.on("error", (err) => {
|
|
92
|
+
console.error("dev-engine: WebSocket connection error:", err);
|
|
93
|
+
broker.removeConnection(conn);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const GLOBAL_PLATFORM_KEY = "__sync_dev_platform__";
|
|
97
|
+
async function getPlatform() {
|
|
98
|
+
const g = globalThis;
|
|
99
|
+
if (!g[GLOBAL_PLATFORM_KEY]) {
|
|
100
|
+
try {
|
|
101
|
+
console.log("dev-engine: calling getPlatformProxy()...");
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
const { getPlatformProxy } = await import("wrangler");
|
|
104
|
+
const platform = await getPlatformProxy();
|
|
105
|
+
g[GLOBAL_PLATFORM_KEY] = { platform };
|
|
106
|
+
console.log(`dev-engine: getPlatformProxy() succeeded in ${Date.now() - startTime}ms`);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error("dev-engine: Failed to load wrangler platform proxy:", err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return g[GLOBAL_PLATFORM_KEY]?.platform;
|
|
113
|
+
}
|
|
114
|
+
export async function broadcastExternalChange(channel, action, key, data) {
|
|
115
|
+
const broker = getDevBroker();
|
|
116
|
+
await broker.handleExternalChange(channel, action, key, data);
|
|
117
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
import { SyncBroker } from "./broker.js";
|
|
3
|
+
export declare class SyncEngineBase extends DurableObject<Env> {
|
|
4
|
+
protected broker: SyncBroker;
|
|
5
|
+
private connMap;
|
|
6
|
+
constructor(ctx: DurableObjectState, env: Env, handlers: any[]);
|
|
7
|
+
fetch(request: Request): Promise<Response>;
|
|
8
|
+
private connectWebSocket;
|
|
9
|
+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
10
|
+
webSocketClose(ws: WebSocket, code: number, reason: string): void;
|
|
11
|
+
webSocketError(ws: WebSocket): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/server/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAwB,MAAM,aAAa,CAAC;AA4C/D,qBAAa,cAAe,SAAQ,aAAa,CAAC,GAAG,CAAC;IACpD,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC;IAC7B,OAAO,CAAC,OAAO,CAAkD;gBAErD,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE;IAKxD,KAAK,CAAC,OAAO,EAAE,OAAO;IAuB5B,OAAO,CAAC,gBAAgB;IA2BlB,gBAAgB,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW;IAiBnE,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAa1D,cAAc,CAAC,EAAE,EAAE,SAAS;CAO7B"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
import { SyncBroker } from "./broker.js";
|
|
3
|
+
class CloudflareSyncConnection {
|
|
4
|
+
ws;
|
|
5
|
+
auth = null;
|
|
6
|
+
subscribedChannels = new Set();
|
|
7
|
+
headers;
|
|
8
|
+
url;
|
|
9
|
+
constructor(ws, request) {
|
|
10
|
+
this.ws = ws;
|
|
11
|
+
this.headers = new Headers(request.headers);
|
|
12
|
+
this.url = request.url;
|
|
13
|
+
}
|
|
14
|
+
send(data) {
|
|
15
|
+
try {
|
|
16
|
+
this.ws.send(data);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Ignore sending to closed sockets
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
close(code, reason) {
|
|
23
|
+
try {
|
|
24
|
+
this.ws.close(code, reason);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Ignore errors on close
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
getAuth() {
|
|
31
|
+
return this.auth;
|
|
32
|
+
}
|
|
33
|
+
setAuth(newAuth) {
|
|
34
|
+
this.auth = newAuth;
|
|
35
|
+
}
|
|
36
|
+
getSubscribedChannels() {
|
|
37
|
+
return this.subscribedChannels;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class SyncEngineBase extends DurableObject {
|
|
41
|
+
broker;
|
|
42
|
+
connMap = new Map();
|
|
43
|
+
constructor(ctx, env, handlers) {
|
|
44
|
+
super(ctx, env);
|
|
45
|
+
this.broker = new SyncBroker(handlers);
|
|
46
|
+
}
|
|
47
|
+
async fetch(request) {
|
|
48
|
+
const url = new URL(request.url);
|
|
49
|
+
if (url.pathname === "/websocket") {
|
|
50
|
+
return this.connectWebSocket(request);
|
|
51
|
+
}
|
|
52
|
+
if (url.pathname === "/broadcast" && request.method === "POST") {
|
|
53
|
+
try {
|
|
54
|
+
const body = (await request.json());
|
|
55
|
+
const { channel, action, key, data } = body;
|
|
56
|
+
await this.broker.handleExternalChange(channel, action, key, data);
|
|
57
|
+
return new Response(null, { status: 204 });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return new Response(err.message || "Error processing broadcast", {
|
|
61
|
+
status: 400,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return new Response("Not found", { status: 404 });
|
|
66
|
+
}
|
|
67
|
+
connectWebSocket(request) {
|
|
68
|
+
if (request.headers.get("Upgrade") !== "websocket") {
|
|
69
|
+
return new Response("Expected Upgrade: websocket", { status: 426 });
|
|
70
|
+
}
|
|
71
|
+
const [client, server] = Object.values(new WebSocketPair());
|
|
72
|
+
this.ctx.acceptWebSocket(server);
|
|
73
|
+
const conn = new CloudflareSyncConnection(server, request);
|
|
74
|
+
const url = new URL(request.url);
|
|
75
|
+
const userId = url.searchParams.get("userId") || request.headers.get("x-user-id");
|
|
76
|
+
if (userId) {
|
|
77
|
+
conn.setAuth({ userId });
|
|
78
|
+
}
|
|
79
|
+
this.connMap.set(server, conn);
|
|
80
|
+
this.broker.registerConnection(conn);
|
|
81
|
+
return new Response(null, {
|
|
82
|
+
status: 101,
|
|
83
|
+
webSocket: client,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async webSocketMessage(ws, message) {
|
|
87
|
+
const conn = this.connMap.get(ws);
|
|
88
|
+
if (!conn)
|
|
89
|
+
return;
|
|
90
|
+
if (typeof message !== "string")
|
|
91
|
+
return;
|
|
92
|
+
const request = new Request(conn.url, {
|
|
93
|
+
headers: conn.headers,
|
|
94
|
+
});
|
|
95
|
+
await this.broker.handleMessage(conn, message, this.env, request);
|
|
96
|
+
}
|
|
97
|
+
webSocketClose(ws, code, reason) {
|
|
98
|
+
const conn = this.connMap.get(ws);
|
|
99
|
+
if (conn) {
|
|
100
|
+
this.broker.removeConnection(conn);
|
|
101
|
+
this.connMap.delete(ws);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
ws.close(code, reason);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Ignore
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
webSocketError(ws) {
|
|
111
|
+
const conn = this.connMap.get(ws);
|
|
112
|
+
if (conn) {
|
|
113
|
+
this.broker.removeConnection(conn);
|
|
114
|
+
this.connMap.delete(ws);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function publishEvent(channel: string, action: "create" | "update" | "delete", key: string | undefined, data: any): Promise<void>;
|
|
2
|
+
export declare function handleUpgrade(request: Request, platform: App.Platform | undefined): Promise<Response>;
|
|
3
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/server/handler.ts"],"names":[],"mappings":"AAAA,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG,iBAgCV;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,GACjC,OAAO,CAAC,QAAQ,CAAC,CAqBnB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export async function publishEvent(channel, action, key, data) {
|
|
2
|
+
const envId = "$app/environment";
|
|
3
|
+
let isDev = false;
|
|
4
|
+
try {
|
|
5
|
+
const env = await import(/* @vite-ignore */ envId);
|
|
6
|
+
isDev = env.dev;
|
|
7
|
+
}
|
|
8
|
+
catch { }
|
|
9
|
+
if (isDev) {
|
|
10
|
+
const { broadcastExternalChange } = await import("./dev-engine.js");
|
|
11
|
+
await broadcastExternalChange(channel, action, key, data);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const serverId = "$app/server";
|
|
16
|
+
const { getRequestEvent } = await import(/* @vite-ignore */ serverId);
|
|
17
|
+
const { platform } = getRequestEvent();
|
|
18
|
+
const namespace = platform?.env.SYNC_ENGINE;
|
|
19
|
+
if (!namespace)
|
|
20
|
+
return;
|
|
21
|
+
const id = namespace.idFromName("global");
|
|
22
|
+
const stub = namespace.get(id);
|
|
23
|
+
await stub.fetch("https://realtime.internal/broadcast", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ channel, action, key, data }),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.error("Failed to publish sync event to Durable Object:", err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function handleUpgrade(request, platform) {
|
|
34
|
+
if (request.headers.get("Upgrade") !== "websocket") {
|
|
35
|
+
return new Response("Expected Upgrade: websocket", { status: 426 });
|
|
36
|
+
}
|
|
37
|
+
const namespace = platform?.env?.SYNC_ENGINE;
|
|
38
|
+
if (!namespace) {
|
|
39
|
+
return new Response("SyncEngine binding is not available", { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const id = namespace.idFromName("global");
|
|
43
|
+
const stub = namespace.get(id);
|
|
44
|
+
return await stub.fetch(new Request("https://realtime.internal/websocket", request));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return new Response(err.message || "SyncEngine binding is not available", {
|
|
48
|
+
status: 503,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ZodSchema } from "zod";
|
|
2
|
+
export type SyncContext = {
|
|
3
|
+
platform: App.Platform | undefined;
|
|
4
|
+
request: Request;
|
|
5
|
+
auth?: any;
|
|
6
|
+
};
|
|
7
|
+
export type SyncHandlerConfig<TRow = any> = {
|
|
8
|
+
channel: string | ((ctx: SyncContext) => string);
|
|
9
|
+
fetch: (ctx: SyncContext, since?: string) => Promise<TRow[]>;
|
|
10
|
+
create?: (ctx: SyncContext, data: TRow) => Promise<TRow>;
|
|
11
|
+
update?: (ctx: SyncContext, key: string, changes: Partial<TRow>) => Promise<TRow>;
|
|
12
|
+
delete?: (ctx: SyncContext, key: string) => Promise<void>;
|
|
13
|
+
authorize?: (ctx: SyncContext) => Promise<void>;
|
|
14
|
+
validate?: {
|
|
15
|
+
create?: ZodSchema<any>;
|
|
16
|
+
update?: ZodSchema<any>;
|
|
17
|
+
};
|
|
18
|
+
scope?: (ctx: SyncContext, action: "create" | "update" | "delete", data: any) => Promise<string[] | "all"> | string[] | "all";
|
|
19
|
+
};
|
|
20
|
+
export interface SyncHandler<TRow = any> {
|
|
21
|
+
config: SyncHandlerConfig<TRow>;
|
|
22
|
+
resolveChannel(ctx: SyncContext): string;
|
|
23
|
+
}
|
|
24
|
+
export declare function defineSync<TRow = any>(config: SyncHandlerConfig<TRow>): SyncHandler<TRow>;
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAErC,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,SAAS,CAAC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,iBAAiB,CAAC,IAAI,GAAG,GAAG,IAAI;IAC1C,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,MAAM,CAAC,CAAC;IACjD,KAAK,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7D,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,CAAC,EAAE,CACP,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;QACxB,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,CACN,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,IAAI,EAAE,GAAG,KACN,OAAO,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC;CACnD,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,IAAI,GAAG,GAAG;IACrC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAChC,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAAC;CAC1C;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAG,GAAG,EACnC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,GAC9B,WAAW,CAAC,IAAI,CAAC,CASnB"}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAanC,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,CA4EpE"}
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const SYNC_PATH = "/api/sync";
|
|
2
|
+
export function syncDevPlugin(options) {
|
|
3
|
+
const handlersPath = options?.handlersPath ?? "/src/lib/server/sync-handlers.ts";
|
|
4
|
+
return {
|
|
5
|
+
name: "sync-dev-websocket",
|
|
6
|
+
apply: "serve",
|
|
7
|
+
async configureServer(server) {
|
|
8
|
+
// Dynamic import — ws is a transitive dependency of Vite itself.
|
|
9
|
+
// @ts-expect-error -- ws has no bundled type declarations
|
|
10
|
+
const { WebSocketServer } = (await import("ws"));
|
|
11
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
12
|
+
server.httpServer?.on("upgrade", (request, socket, head) => {
|
|
13
|
+
const url = new URL(request.url ?? "", `http://${request.headers.host}`);
|
|
14
|
+
if (url.pathname !== SYNC_PATH) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
wss.handleUpgrade(request, socket, head, (client) => {
|
|
18
|
+
console.log("sync-dev-plugin: WebSocket upgrade handler starting (buffering messages)...");
|
|
19
|
+
// Buffer messages while async ssrLoadModule runs to avoid race conditions
|
|
20
|
+
const messageQueue = [];
|
|
21
|
+
const onMessage = (data) => {
|
|
22
|
+
messageQueue.push(data);
|
|
23
|
+
};
|
|
24
|
+
client.on("message", onMessage);
|
|
25
|
+
(async () => {
|
|
26
|
+
try {
|
|
27
|
+
// Load the handlers module dynamically from the user-configured path
|
|
28
|
+
const handlersModule = await server.ssrLoadModule(handlersPath);
|
|
29
|
+
const devEngine = await server.ssrLoadModule("@sveltebase/sync/server/dev-engine");
|
|
30
|
+
// Register handlers dynamically before connecting client
|
|
31
|
+
devEngine.setHandlers(handlersModule.handlers);
|
|
32
|
+
// Remove temporary buffering listener
|
|
33
|
+
client.off("message", onMessage);
|
|
34
|
+
// Register client in the dev engine
|
|
35
|
+
devEngine.addClient(client, request);
|
|
36
|
+
console.log("sync-dev-plugin: WebSocket upgrade handler completed, replaying buffered messages:", messageQueue.length);
|
|
37
|
+
// Replay any buffered messages
|
|
38
|
+
for (const msg of messageQueue) {
|
|
39
|
+
client.emit("message", msg);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error("sync-dev-plugin: Error in WebSocket upgrade handler:", err);
|
|
44
|
+
try {
|
|
45
|
+
client.off("message", onMessage);
|
|
46
|
+
client.close(1011, "Internal server error");
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|