@sveltebase/sync 1.4.1 → 1.4.3

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 CHANGED
@@ -1,32 +1,28 @@
1
1
  # @sveltebase/sync
2
2
 
3
- Reactive, local-first database synchronization for Svelte 5 using a separate Cloudflare Worker for realtime sync.
3
+ Reactive, local-first database synchronization for Svelte 5 on Cloudflare Workers.
4
4
 
5
5
  ## Architecture
6
6
 
7
- `@sveltebase/sync` now uses two Workers:
7
+ `@sveltebase/sync` uses one Cloudflare Worker:
8
8
 
9
9
  ```txt
10
10
  browser
11
11
  -> SvelteKit app Worker
12
- /api/sync -> env.SYNC_WORKER.fetch(request)
13
- -> sync Worker
14
- owns SyncEngine Durable Object
15
- owns websocket upgrades
16
- owns broadcasts
17
- owns sync database/runtime bindings
18
- owns sync auth verification
12
+ /api/sync -> SyncEngine Durable Object
13
+ server writes -> platform.env.SYNC_ENGINE
19
14
  ```
20
15
 
21
- The SvelteKit app Worker does not export sync Durable Objects. This lets apps use the official `@sveltejs/adapter-cloudflare`.
16
+ The production Worker wraps the official `@sveltejs/adapter-cloudflare` output and exports the `SyncEngine` Durable Object. Vite dev uses `syncDevPlugin()` for the WebSocket broker and `adapter-cloudflare` platform proxy for local Cloudflare bindings.
22
17
 
23
18
  ## Imports
24
19
 
25
20
  ```ts
26
21
  import { SyncClient, createLiveQuery } from "@sveltebase/sync/client";
27
22
  import { defineSync, createPublisher } from "@sveltebase/sync/server";
28
- import { syncProxy } from "@sveltebase/sync/sveltekit";
29
- import { defineSyncWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
23
+ import { syncEngineRoute } from "@sveltebase/sync/sveltekit";
24
+ import { createSyncAppWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
25
+ import { syncDevPlugin } from "@sveltebase/sync/vite";
30
26
  ```
31
27
 
32
28
  ## Client
@@ -35,16 +31,7 @@ import { defineSyncWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
35
31
  // src/lib/sync-client.ts
36
32
  import { SyncClient } from "@sveltebase/sync/client";
37
33
 
38
- type AppSchema = {
39
- todos: {
40
- id: string;
41
- title: string;
42
- completed: boolean;
43
- updatedAt: string;
44
- };
45
- };
46
-
47
- export const sync = new SyncClient<AppSchema>({
34
+ export const sync = new SyncClient({
48
35
  name: "app-sync",
49
36
  url: "/api/sync",
50
37
  tables: {
@@ -58,7 +45,7 @@ export const sync = new SyncClient<AppSchema>({
58
45
 
59
46
  ## Sync Handlers
60
47
 
61
- Handlers run in the sync Worker. Use `ctx.platform.env` for Cloudflare bindings, `ctx.auth` for verified auth data, and `ctx.identity` for ownership/scoped fanout.
48
+ Handlers run in the sync engine. Use `ctx.platform.env` for Cloudflare bindings, `ctx.auth` for verified auth data, and `ctx.identity` for ownership/scoped fanout.
62
49
 
63
50
  ```ts
64
51
  // src/lib/server/sync-handlers.ts
@@ -69,14 +56,11 @@ export const todoSync = defineSync({
69
56
 
70
57
  fetch: async (ctx, since) => {
71
58
  const db = ctx.platform.env.DB;
72
- // Query any database here.
73
59
  return [];
74
60
  },
75
61
 
76
62
  authorize: async (ctx) => {
77
- if (!ctx.auth) {
78
- throw new Error("Unauthorized");
79
- }
63
+ if (!ctx.auth) throw new Error("Unauthorized");
80
64
  },
81
65
 
82
66
  scope: (ctx) => {
@@ -87,97 +71,113 @@ export const todoSync = defineSync({
87
71
  export const handlers = [todoSync];
88
72
  ```
89
73
 
90
- ## Sync Worker
91
-
92
- Create a standalone Worker entrypoint that owns the Durable Object:
74
+ ## SvelteKit Route For Vite Dev
93
75
 
94
76
  ```ts
95
- // src/worker/sync.ts
77
+ // src/routes/api/sync/+server.ts
96
78
  import { jwtCookieAuth } from "@sveltebase/auth/sync";
97
- import { defineSyncWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
79
+ import { syncEngineRoute } from "@sveltebase/sync/sveltekit";
98
80
  import { handlers } from "$lib/server/sync-handlers";
99
81
 
100
- export default defineSyncWorker({
82
+ export const { GET } = syncEngineRoute({
101
83
  handlers,
102
84
  auth: jwtCookieAuth(),
85
+ allowUnauthenticated: true,
103
86
  });
104
-
105
- export { SyncEngine };
106
87
  ```
107
88
 
108
- `defineSyncWorker()` handles:
109
-
110
- - `GET /api/sync`: public websocket upgrade endpoint
111
- - `POST /broadcast`: publish one external change
112
- - `POST /broadcast-batch`: publish a batch of external changes
113
-
114
- `GET /websocket` is internal to the sync Worker and Durable Object.
115
-
116
- ## SvelteKit Proxy Route
117
-
118
- Keep browsers connecting to the app origin so existing cookies are sent:
89
+ ## Worker Wrapper For Wrangler And Production
119
90
 
120
91
  ```ts
121
- // src/routes/api/sync/+server.ts
122
- import { SYNC_WORKER_URL } from "$env/static/private";
123
- import { syncProxy } from "@sveltebase/sync/sveltekit";
92
+ // src/worker/app.ts
93
+ import app from "../../.svelte-kit/cloudflare/_worker.js";
94
+ import { jwtCookieAuth } from "@sveltebase/auth/sync";
95
+ import { createSyncAppWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
96
+ import { handlers } from "$lib/server/sync-handlers";
124
97
 
125
- export const { GET, POST } = syncProxy({
126
- fallbackUrl: SYNC_WORKER_URL,
98
+ export default createSyncAppWorker(app, {
99
+ handlers,
100
+ auth: jwtCookieAuth(),
101
+ allowUnauthenticated: true,
127
102
  });
103
+
104
+ export { SyncEngine };
128
105
  ```
129
106
 
130
- In production, configure a Cloudflare service binding named `SYNC_WORKER`. In local development, use `fallbackUrl` such as `http://localhost:8788/api/sync`.
107
+ `createSyncAppWorker()` handles `GET /api/sync` and internal broadcast routes, then delegates all other requests to the adapter output.
131
108
 
132
109
  ## Publishing Server Events
133
110
 
134
- Publishing is explicit. It never reads SvelteKit request context implicitly.
111
+ Publishing is explicit and targets the one-worker Durable Object binding. During Vite dev, `syncDevPlugin()` provides an in-process broker fallback.
135
112
 
136
113
  ```ts
137
114
  import { createPublisher } from "@sveltebase/sync/server";
138
115
 
139
- type AppSchema = {
140
- todos: { id: string; title: string; updatedAt: string };
141
- };
142
-
143
- const publish = createPublisher<AppSchema>({
116
+ const publish = createPublisher({
144
117
  platform: ctx.platform,
145
- binding: "SYNC_WORKER",
146
- fallbackUrl: env.SYNC_WORKER_URL,
147
118
  });
148
119
 
149
120
  await publish("todos", "update", todo.id, todo);
150
121
  ```
151
122
 
152
- Inside the sync Worker, `createPublisher()` publishes directly to `platform.env.SYNC_ENGINE`. Inside the app Worker, it publishes through `platform.env.SYNC_WORKER.fetch()`. Without a binding, it uses `fallbackUrl`.
123
+ ## Vite Dev
153
124
 
154
- ## Cloudflare Configuration
125
+ ```ts
126
+ // vite.config.ts
127
+ import { sveltekit } from "@sveltejs/kit/vite";
128
+ import { syncDevPlugin } from "@sveltebase/sync/vite";
129
+ import { jwtCookieAuth } from "@sveltebase/auth/sync";
130
+ import { defineConfig } from "vite";
131
+
132
+ export default defineConfig({
133
+ plugins: [
134
+ syncDevPlugin({
135
+ auth: jwtCookieAuth(),
136
+ allowUnauthenticated: true,
137
+ wranglerConfigPath: "wrangler.local.jsonc",
138
+ }),
139
+ sveltekit(),
140
+ ],
141
+ });
142
+ ```
155
143
 
156
- App Worker:
144
+ ## Svelte Config
145
+
146
+ ```js
147
+ // svelte.config.js
148
+ import adapter from "@sveltejs/adapter-cloudflare";
149
+ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
150
+
151
+ export default {
152
+ preprocess: vitePreprocess(),
153
+ kit: {
154
+ adapter: adapter({
155
+ platformProxy: {
156
+ configPath: "wrangler.local.jsonc",
157
+ },
158
+ }),
159
+ },
160
+ };
161
+ ```
162
+
163
+ ## Wrangler Configuration
164
+
165
+ Main config for Wrangler dev with remote bindings and production deploy:
157
166
 
158
167
  ```jsonc
168
+ // wrangler.jsonc
159
169
  {
160
170
  "name": "my-app",
161
- "main": ".svelte-kit/cloudflare/_worker.js",
171
+ "main": "src/worker/app.ts",
162
172
  "compatibility_date": "2026-06-07",
163
173
  "compatibility_flags": ["nodejs_compat"],
164
- "services": [
174
+ "d1_databases": [
165
175
  {
166
- "binding": "SYNC_WORKER",
167
- "service": "my-app-sync"
176
+ "binding": "DB",
177
+ "database_name": "my-app",
178
+ "database_id": "..."
168
179
  }
169
- ]
170
- }
171
- ```
172
-
173
- Sync Worker:
174
-
175
- ```jsonc
176
- {
177
- "name": "my-app-sync",
178
- "main": "./src/worker/sync.ts",
179
- "compatibility_date": "2026-06-07",
180
- "compatibility_flags": ["nodejs_compat"],
180
+ ],
181
181
  "durable_objects": {
182
182
  "bindings": [
183
183
  {
@@ -195,9 +195,23 @@ Sync Worker:
195
195
  }
196
196
  ```
197
197
 
198
- Both Workers need the same session secret when using `@sveltebase/auth/sync`:
198
+ Local config for adapter platform proxy in Vite dev:
199
199
 
200
- ```bash
201
- wrangler secret put JWT_SECRET --config wrangler.jsonc
202
- wrangler secret put JWT_SECRET --config wrangler.sync.jsonc
200
+ ```jsonc
201
+ // wrangler.local.jsonc
202
+ {
203
+ "name": "my-app-local",
204
+ "main": ".svelte-kit/cloudflare/_worker.js",
205
+ "compatibility_date": "2026-06-07",
206
+ "compatibility_flags": ["nodejs_compat"],
207
+ "d1_databases": [
208
+ {
209
+ "binding": "DB",
210
+ "database_name": "my-app",
211
+ "database_id": "..."
212
+ }
213
+ ]
214
+ }
203
215
  ```
216
+
217
+ Use `wrangler secret put JWT_SECRET --config wrangler.jsonc` for remote/prod secrets. Use `.env` for Vite dev secrets loaded by the platform proxy.
@@ -0,0 +1,14 @@
1
+ import { type SyncAuthResult } from "../server/handler.js";
2
+ import type { SyncHandler, SyncPlatform } from "../server/index.js";
3
+ export declare function configureSyncEngine(handlers: SyncHandler[]): void;
4
+ export declare function getSyncEngineHandlers(): SyncHandler<any, any>[];
5
+ export type SyncWorkerOptions<TAuth = unknown> = {
6
+ handlers: SyncHandler[];
7
+ durableObjectBinding?: string;
8
+ websocketPath?: string;
9
+ auth?: (request: Request, platform: SyncPlatform) => Promise<SyncAuthResult<TAuth>> | SyncAuthResult<TAuth>;
10
+ identity?: (auth: TAuth) => string | number | bigint | null | undefined;
11
+ allowUnauthenticated?: boolean;
12
+ };
13
+ export declare function handleSyncRequest<TAuth = unknown>(request: Request, env: Record<string, unknown>, ctx: ExecutionContext, options: SyncWorkerOptions<TAuth>): Promise<Response>;
14
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/cloudflare/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA2CpE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,QAE1D;AAED,wBAAgB,qBAAqB,4BAEpC;AAED,MAAM,MAAM,iBAAiB,CAAC,KAAK,GAAG,OAAO,IAAI;IAC/C,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,CACL,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,YAAY,KACnB,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC5D,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxE,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AA6FF,wBAAsB,iBAAiB,CAAC,KAAK,GAAG,OAAO,EACrD,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,qBA2ClC"}
@@ -0,0 +1,108 @@
1
+ import { INTERNAL_AUTH_HEADER, } from "../server/handler.js";
2
+ let activeHandlers = [];
3
+ function defaultIdentity(auth) {
4
+ const value = auth?.identity ?? auth?.user?.id ?? auth?.userId;
5
+ return value == null ? null : String(value);
6
+ }
7
+ function serializeConnectionAuth(auth, identity) {
8
+ const payload = { auth, identity };
9
+ return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
10
+ }
11
+ function createPlatform(request, env, ctx) {
12
+ return {
13
+ env,
14
+ ctx,
15
+ context: ctx,
16
+ caches,
17
+ cf: request.cf,
18
+ };
19
+ }
20
+ function withPath(request, pathname) {
21
+ const url = new URL(request.url);
22
+ url.pathname = pathname;
23
+ return url.toString();
24
+ }
25
+ function isWebSocketRequest(request) {
26
+ return request.headers.get("Upgrade")?.toLowerCase() === "websocket";
27
+ }
28
+ export function configureSyncEngine(handlers) {
29
+ activeHandlers = handlers;
30
+ }
31
+ export function getSyncEngineHandlers() {
32
+ return activeHandlers;
33
+ }
34
+ async function forwardToEngine(request, env, durableObjectBinding) {
35
+ const namespace = env[durableObjectBinding];
36
+ if (!namespace) {
37
+ return new Response(`Missing ${durableObjectBinding} Durable Object binding`, { status: 500 });
38
+ }
39
+ const id = namespace.idFromName("global");
40
+ return namespace.get(id).fetch(request);
41
+ }
42
+ async function handleWebSocket(request, env, ctx, options) {
43
+ if (!isWebSocketRequest(request)) {
44
+ return new Response("Expected Upgrade: websocket", { status: 426 });
45
+ }
46
+ const publicHeaders = new Headers(request.headers);
47
+ publicHeaders.delete(INTERNAL_AUTH_HEADER);
48
+ const publicRequest = new Request(request, {
49
+ headers: publicHeaders,
50
+ });
51
+ const platform = createPlatform(publicRequest, env, ctx);
52
+ let resolvedAuth = null;
53
+ let identity = null;
54
+ if (options.auth) {
55
+ resolvedAuth = (await options.auth(publicRequest, platform)) ?? null;
56
+ if (!resolvedAuth && options.allowUnauthenticated === false) {
57
+ return new Response("Unauthorized", { status: 401 });
58
+ }
59
+ if (resolvedAuth) {
60
+ const identityValue = options.identity
61
+ ? options.identity(resolvedAuth)
62
+ : defaultIdentity(resolvedAuth);
63
+ identity = identityValue == null ? null : String(identityValue);
64
+ }
65
+ }
66
+ else if (options.allowUnauthenticated === false) {
67
+ return new Response("Unauthorized", { status: 401 });
68
+ }
69
+ const forwardedHeaders = new Headers(publicRequest.headers);
70
+ forwardedHeaders.delete(INTERNAL_AUTH_HEADER);
71
+ if (resolvedAuth) {
72
+ forwardedHeaders.set(INTERNAL_AUTH_HEADER, serializeConnectionAuth(resolvedAuth, identity));
73
+ }
74
+ const forwardedRequest = new Request(withPath(publicRequest, "/websocket"), publicRequest);
75
+ for (const [key, value] of forwardedHeaders) {
76
+ forwardedRequest.headers.set(key, value);
77
+ }
78
+ return forwardToEngine(forwardedRequest, env, options.durableObjectBinding);
79
+ }
80
+ export async function handleSyncRequest(request, env, ctx, options) {
81
+ configureSyncEngine(options.handlers);
82
+ const url = new URL(request.url);
83
+ const durableObjectBinding = options.durableObjectBinding ?? "SYNC_ENGINE";
84
+ const websocketPath = options.websocketPath ?? "/api/sync";
85
+ const authMetadata = options.auth;
86
+ const allowUnauthenticated = options.allowUnauthenticated ??
87
+ authMetadata?.allowUnauthenticated ??
88
+ true;
89
+ if (url.pathname === websocketPath && request.method === "GET") {
90
+ return handleWebSocket(request, env, ctx, {
91
+ ...options,
92
+ durableObjectBinding,
93
+ websocketPath,
94
+ allowUnauthenticated,
95
+ });
96
+ }
97
+ if (url.pathname === "/websocket") {
98
+ return new Response("Not found", { status: 404 });
99
+ }
100
+ if ((url.pathname === "/broadcast" ||
101
+ url.pathname === "/broadcast-batch") &&
102
+ request.method === "POST") {
103
+ const headers = new Headers(request.headers);
104
+ headers.delete(INTERNAL_AUTH_HEADER);
105
+ return forwardToEngine(new Request(request, { headers }), env, durableObjectBinding);
106
+ }
107
+ return new Response("Not found", { status: 404 });
108
+ }
@@ -1,16 +1,10 @@
1
1
  import { SyncEngineBase } from "../server/engine.js";
2
- import { type SyncAuthResult } from "../server/handler.js";
3
- import type { SyncHandler, SyncPlatform } from "../server/index.js";
4
- export type SyncWorkerOptions<TAuth = unknown> = {
5
- handlers: SyncHandler[];
6
- durableObjectBinding?: string;
7
- websocketPath?: string;
8
- auth?: (request: Request, platform: SyncPlatform) => Promise<SyncAuthResult<TAuth>> | SyncAuthResult<TAuth>;
9
- identity?: (auth: TAuth) => string | number | bigint | null | undefined;
10
- allowUnauthenticated?: boolean;
2
+ import { type SyncWorkerOptions } from "./handler.js";
3
+ export { configureSyncEngine, handleSyncRequest, type SyncWorkerOptions, } from "./handler.js";
4
+ export type SyncAppWorker = {
5
+ fetch: NonNullable<ExportedHandler["fetch"]>;
11
6
  };
12
- export declare function createSyncWorker<TAuth = unknown>(options: SyncWorkerOptions<TAuth>): ExportedHandler;
13
- export declare function defineSyncWorker<TAuth = unknown>(options: SyncWorkerOptions<TAuth>): ExportedHandler;
7
+ export declare function createSyncAppWorker<TAuth = unknown>(app: SyncAppWorker, options: SyncWorkerOptions<TAuth>): ExportedHandler;
14
8
  export declare class SyncEngine extends SyncEngineBase {
15
9
  constructor(ctx: DurableObjectState, env: Record<string, unknown>);
16
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA2CpE,MAAM,MAAM,iBAAiB,CAAC,KAAK,GAAG,OAAO,IAAI;IAC/C,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,CACL,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,YAAY,KACnB,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC5D,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxE,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AA6FF,wBAAgB,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAC9C,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAChC,eAAe,CAgDjB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAC9C,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAChC,eAAe,CAEjB;AAED,qBAAa,UAAW,SAAQ,cAAc;gBAChC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGlE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;CAC9C,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,GAAG,OAAO,EACjD,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAChC,eAAe,CAwBjB;AAED,qBAAa,UAAW,SAAQ,cAAc;gBAChC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGlE"}
@@ -1,116 +1,24 @@
1
1
  import { SyncEngineBase } from "../server/engine.js";
2
- import { INTERNAL_AUTH_HEADER, } from "../server/handler.js";
3
- let activeHandlers = [];
4
- function defaultIdentity(auth) {
5
- const value = auth?.identity ?? auth?.user?.id ?? auth?.userId;
6
- return value == null ? null : String(value);
7
- }
8
- function serializeConnectionAuth(auth, identity) {
9
- const payload = { auth, identity };
10
- return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
11
- }
12
- function createPlatform(request, env, ctx) {
13
- return {
14
- env,
15
- ctx,
16
- context: ctx,
17
- caches,
18
- cf: request.cf,
19
- };
20
- }
21
- function withPath(request, pathname) {
22
- const url = new URL(request.url);
23
- url.pathname = pathname;
24
- return url.toString();
25
- }
26
- function isWebSocketRequest(request) {
27
- return request.headers.get("Upgrade")?.toLowerCase() === "websocket";
28
- }
29
- async function forwardToEngine(request, env, durableObjectBinding) {
30
- const namespace = env[durableObjectBinding];
31
- if (!namespace) {
32
- return new Response(`Missing ${durableObjectBinding} Durable Object binding`, { status: 500 });
33
- }
34
- const id = namespace.idFromName("global");
35
- return namespace.get(id).fetch(request);
36
- }
37
- async function handleWebSocket(request, env, ctx, options) {
38
- if (!isWebSocketRequest(request)) {
39
- return new Response("Expected Upgrade: websocket", { status: 426 });
40
- }
41
- const publicHeaders = new Headers(request.headers);
42
- publicHeaders.delete(INTERNAL_AUTH_HEADER);
43
- const publicRequest = new Request(request, {
44
- headers: publicHeaders,
45
- });
46
- const platform = createPlatform(publicRequest, env, ctx);
47
- let resolvedAuth = null;
48
- let identity = null;
49
- if (options.auth) {
50
- resolvedAuth = (await options.auth(publicRequest, platform)) ?? null;
51
- if (!resolvedAuth && options.allowUnauthenticated === false) {
52
- return new Response("Unauthorized", { status: 401 });
53
- }
54
- if (resolvedAuth) {
55
- const identityValue = options.identity
56
- ? options.identity(resolvedAuth)
57
- : defaultIdentity(resolvedAuth);
58
- identity = identityValue == null ? null : String(identityValue);
59
- }
60
- }
61
- else if (options.allowUnauthenticated === false) {
62
- return new Response("Unauthorized", { status: 401 });
63
- }
64
- const forwardedHeaders = new Headers(publicRequest.headers);
65
- forwardedHeaders.delete(INTERNAL_AUTH_HEADER);
66
- if (resolvedAuth) {
67
- forwardedHeaders.set(INTERNAL_AUTH_HEADER, serializeConnectionAuth(resolvedAuth, identity));
68
- }
69
- const forwardedRequest = new Request(withPath(publicRequest, "/websocket"), publicRequest);
70
- for (const [key, value] of forwardedHeaders) {
71
- forwardedRequest.headers.set(key, value);
72
- }
73
- return forwardToEngine(forwardedRequest, env, options.durableObjectBinding);
74
- }
75
- export function createSyncWorker(options) {
76
- activeHandlers = options.handlers;
77
- const durableObjectBinding = options.durableObjectBinding ?? "SYNC_ENGINE";
78
- const websocketPath = options.websocketPath ?? "/api/sync";
79
- const authMetadata = options.auth;
80
- const allowUnauthenticated = options.allowUnauthenticated ??
81
- authMetadata?.allowUnauthenticated ??
82
- true;
2
+ import { configureSyncEngine, getSyncEngineHandlers, handleSyncRequest, } from "./handler.js";
3
+ export { configureSyncEngine, handleSyncRequest, } from "./handler.js";
4
+ export function createSyncAppWorker(app, options) {
5
+ configureSyncEngine(options.handlers);
83
6
  return {
84
7
  async fetch(request, env, ctx) {
85
8
  const url = new URL(request.url);
86
- const workerEnv = env;
87
- if (url.pathname === websocketPath && request.method === "GET") {
88
- return handleWebSocket(request, workerEnv, ctx, {
89
- ...options,
90
- durableObjectBinding,
91
- websocketPath,
92
- allowUnauthenticated,
93
- });
94
- }
95
- if (url.pathname === "/websocket") {
96
- return new Response("Not found", { status: 404 });
9
+ const websocketPath = options.websocketPath ?? "/api/sync";
10
+ if ((url.pathname === websocketPath && request.method === "GET") ||
11
+ ((url.pathname === "/broadcast" ||
12
+ url.pathname === "/broadcast-batch") &&
13
+ request.method === "POST")) {
14
+ return handleSyncRequest(request, env, ctx, options);
97
15
  }
98
- if ((url.pathname === "/broadcast" ||
99
- url.pathname === "/broadcast-batch") &&
100
- request.method === "POST") {
101
- const headers = new Headers(request.headers);
102
- headers.delete(INTERNAL_AUTH_HEADER);
103
- return forwardToEngine(new Request(request, { headers }), workerEnv, durableObjectBinding);
104
- }
105
- return new Response("Not found", { status: 404 });
16
+ return app.fetch(request, env, ctx);
106
17
  },
107
18
  };
108
19
  }
109
- export function defineSyncWorker(options) {
110
- return createSyncWorker(options);
111
- }
112
20
  export class SyncEngine extends SyncEngineBase {
113
21
  constructor(ctx, env) {
114
- super(ctx, env, activeHandlers);
22
+ super(ctx, env, getSyncEngineHandlers());
115
23
  }
116
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,YAAY,EAAE,MAAM,YAAY,CAAC;AAGzE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,WAAW,IAAI,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC3C,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;gBAE1C,QAAQ,EAAE,WAAW,EAAE;IAK5B,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,OAAO;YA2IJ,eAAe;IAmDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;IAqBE,yBAAyB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;CAkBvF"}
1
+ {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["broker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,YAAY,EAAE,MAAM,YAAY,CAAC;AAGzE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,GAAG,CAAC;IACf,OAAO,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,WAAW,IAAI,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC3C,qBAAqB,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,WAAW,CAAmC;gBAE1C,QAAQ,EAAE,WAAW,EAAE;IAK5B,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE;IAanC,kBAAkB,CAAC,IAAI,EAAE,eAAe;IAIxC,gBAAgB,CAAC,IAAI,EAAE,eAAe;IAI7C;;OAEG;IACH,OAAO,CAAC,WAAW;IAqCN,aAAa,CACxB,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,OAAO;YA2IJ,eAAe;IAmDhB,oBAAoB,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACtC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,GAAG;IAqBE,yBAAyB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;CAkBvF"}
@@ -0,0 +1,22 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { SyncHandler, SyncPlatform } from "./index.js";
3
+ export type SyncDevAuthOptions<TAuth = unknown> = {
4
+ auth?: (request: Request, platform: SyncPlatform) => Promise<TAuth | null | undefined> | TAuth | null | undefined;
5
+ identity?: (auth: TAuth) => string | number | bigint | null | undefined;
6
+ allowUnauthenticated?: boolean;
7
+ platform?: SyncPlatform | (() => Promise<SyncPlatform> | SyncPlatform);
8
+ wranglerConfigPath?: string;
9
+ };
10
+ export declare function setHandlers(handlers: SyncHandler[]): void;
11
+ export declare function addClient(ws: {
12
+ send: (data: string) => void;
13
+ close: (code?: number, reason?: string) => void;
14
+ on: (event: string, listener: (...args: any[]) => void) => void;
15
+ }, req: IncomingMessage, options?: SyncDevAuthOptions): Promise<boolean>;
16
+ export declare function broadcastExternalChange(channel: string, action: "create" | "update" | "delete", key: string | undefined, data: any): Promise<void>;
17
+ export declare function broadcastExternalBatchChange(channel: string, changes: Array<{
18
+ action: "create" | "update" | "delete";
19
+ key?: string;
20
+ data?: any;
21
+ }>): Promise<void>;
22
+ //# sourceMappingURL=dev-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-engine.d.ts","sourceRoot":"","sources":["dev-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAW5D,MAAM,MAAM,kBAAkB,CAAC,KAAK,GAAG,OAAO,IAAI;IAChD,IAAI,CAAC,EAAE,CACL,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,YAAY,KACnB,OAAO,CAAC,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;IAClE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,EAAE,YAAY,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAkBF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QAWlD;AA4ED,wBAAsB,SAAS,CAC7B,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,EACpB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,OAAO,CAAC,CA2ElB;AAED,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;AAED,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC,iBAIH"}
@@ -0,0 +1,165 @@
1
+ import { SyncBroker } from "./broker.js";
2
+ const GLOBAL_BROKER_KEY = "__sveltebase_sync_dev_broker__";
3
+ const GLOBAL_PLATFORM_KEY = "__sveltebase_sync_dev_platform__";
4
+ let devBroker = null;
5
+ function setGlobalBroker(state) {
6
+ const globalObject = globalThis;
7
+ globalObject[GLOBAL_BROKER_KEY] = state;
8
+ }
9
+ function getGlobalBroker() {
10
+ const globalObject = globalThis;
11
+ return globalObject[GLOBAL_BROKER_KEY];
12
+ }
13
+ export function setHandlers(handlers) {
14
+ const existing = getGlobalBroker();
15
+ if (!existing) {
16
+ const state = { broker: new SyncBroker(handlers) };
17
+ setGlobalBroker(state);
18
+ devBroker = state.broker;
19
+ return;
20
+ }
21
+ existing.broker.setHandlers(handlers);
22
+ devBroker = existing.broker;
23
+ }
24
+ function getDevBroker() {
25
+ if (devBroker)
26
+ return devBroker;
27
+ const existing = getGlobalBroker();
28
+ if (existing) {
29
+ devBroker = existing.broker;
30
+ return existing.broker;
31
+ }
32
+ throw new Error("Sync dev broker not initialized. Call setHandlers first.");
33
+ }
34
+ function getHeaderValue(value) {
35
+ if (Array.isArray(value))
36
+ return value[0];
37
+ return value;
38
+ }
39
+ function headersFromIncomingMessage(req) {
40
+ const headers = new Headers();
41
+ for (const [key, value] of Object.entries(req.headers)) {
42
+ if (value === undefined)
43
+ continue;
44
+ if (Array.isArray(value)) {
45
+ for (const item of value)
46
+ headers.append(key, item);
47
+ continue;
48
+ }
49
+ headers.set(key, value);
50
+ }
51
+ return headers;
52
+ }
53
+ function requestFromIncomingMessage(req) {
54
+ const host = getHeaderValue(req.headers.host) ?? "localhost";
55
+ const url = new URL(req.url ?? "", `http://${host}`);
56
+ return new Request(url.toString(), {
57
+ headers: headersFromIncomingMessage(req),
58
+ });
59
+ }
60
+ async function resolvePlatform(options) {
61
+ if (options?.platform) {
62
+ return typeof options.platform === "function"
63
+ ? await options.platform()
64
+ : options.platform;
65
+ }
66
+ const globalObject = globalThis;
67
+ const existing = globalObject[GLOBAL_PLATFORM_KEY];
68
+ if (existing)
69
+ return existing.platform;
70
+ try {
71
+ const { getPlatformProxy } = await import("wrangler");
72
+ const proxy = await getPlatformProxy(options?.wranglerConfigPath
73
+ ? { configPath: options.wranglerConfigPath }
74
+ : undefined);
75
+ const platform = proxy;
76
+ globalObject[GLOBAL_PLATFORM_KEY] = { platform };
77
+ return platform;
78
+ }
79
+ catch {
80
+ const platform = { env: {} };
81
+ globalObject[GLOBAL_PLATFORM_KEY] = { platform };
82
+ return platform;
83
+ }
84
+ }
85
+ function defaultIdentity(auth) {
86
+ const value = auth?.identity ?? auth?.user?.id ?? auth?.userId;
87
+ return value == null ? null : String(value);
88
+ }
89
+ export async function addClient(ws, req, options) {
90
+ const broker = getDevBroker();
91
+ const request = requestFromIncomingMessage(req);
92
+ const platform = await resolvePlatform(options);
93
+ const subscribedChannels = new Set();
94
+ let auth = null;
95
+ let identity = null;
96
+ try {
97
+ if (options?.auth) {
98
+ auth = (await options.auth(request, platform)) ?? null;
99
+ if (!auth && options.allowUnauthenticated === false) {
100
+ ws.close(1008, "Unauthorized");
101
+ return false;
102
+ }
103
+ if (auth) {
104
+ const identityValue = options.identity
105
+ ? options.identity(auth)
106
+ : defaultIdentity(auth);
107
+ identity = identityValue == null ? null : String(identityValue);
108
+ }
109
+ }
110
+ }
111
+ catch {
112
+ ws.close(1011, "Internal server error");
113
+ return false;
114
+ }
115
+ const conn = {
116
+ send(data) {
117
+ ws.send(data);
118
+ },
119
+ close(code, reason) {
120
+ ws.close(code, reason);
121
+ },
122
+ getAuth() {
123
+ return auth;
124
+ },
125
+ setAuth(newAuth) {
126
+ auth = newAuth;
127
+ },
128
+ getIdentity() {
129
+ return identity;
130
+ },
131
+ setIdentity(newIdentity) {
132
+ identity = newIdentity;
133
+ },
134
+ getSubscribedChannels() {
135
+ return subscribedChannels;
136
+ },
137
+ headers: request.headers,
138
+ url: request.url,
139
+ };
140
+ broker.registerConnection(conn);
141
+ ws.on("message", async (data) => {
142
+ const message = typeof data === "string" ? data : String(data);
143
+ try {
144
+ await broker.handleMessage(conn, message, platform, request);
145
+ }
146
+ catch (err) {
147
+ console.error("sync dev engine: error handling message", err);
148
+ }
149
+ });
150
+ ws.on("close", () => {
151
+ broker.removeConnection(conn);
152
+ });
153
+ ws.on("error", () => {
154
+ broker.removeConnection(conn);
155
+ });
156
+ return true;
157
+ }
158
+ export async function broadcastExternalChange(channel, action, key, data) {
159
+ const broker = getDevBroker();
160
+ await broker.handleExternalChange(channel, action, key, data);
161
+ }
162
+ export async function broadcastExternalBatchChange(channel, changes) {
163
+ const broker = getDevBroker();
164
+ await broker.handleExternalBatchChange(channel, changes);
165
+ }
@@ -8,8 +8,6 @@ export type InferSchemaFromHandlers<T extends SyncHandler[]> = {
8
8
  [K in T[number] as K["config"]["channel"] extends string ? K["config"]["channel"] : K["config"]["channel"] extends (...args: any[]) => infer R ? R extends string ? R : string : string]: K extends SyncHandler<infer TRow> ? TRow : never;
9
9
  };
10
10
  export type SyncPublisherOptions = {
11
- binding?: string;
12
- fallbackUrl?: string;
13
11
  durableObjectBinding?: string;
14
12
  platform?: SyncPlatform;
15
13
  };
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;AAE7D,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D,MAAM,MAAM,gBAAgB,CAC1B,OAAO,EACP,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,IAC5C,OAAO,SAAS,QAAQ,GACxB,OAAO,GACP,OAAO,SAAS,QAAQ,GACtB,OAAO,CAAC,OAAO,CAAC,GAChB;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC;AAEzC,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,WAAW,EAAE,IAAI;KAC5D,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,SAAS,MAAM,GACpD,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,GACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC,GACxD,CAAC,SAAS,MAAM,GACd,CAAC,GACD,MAAM,GACR,MAAM,GAAG,CAAC,SAAS,WAAW,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK;CAChE,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAC/D,QAAQ,SAAS,MAAM,OAAO,GAAG,MAAM,EACvC,OAAO,SAAS,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EAE9C,OAAO,EAAE,QAAQ,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,EAC3C,MAAM,EAAE,OAAO,EACf,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,KAC/C,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CACnE,QAAQ,SAAS,MAAM,OAAO,GAAG,MAAM,EAEvC,OAAO,EAAE,QAAQ,GAAG,GAAG,QAAQ,IAAI,MAAM,EAAE,EAC3C,OAAO,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC,KACC,OAAO,CAAC,IAAI,CAAC,CAAC;AAuFnB,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrE,OAAO,EAAE,oBAAoB,GAC5B,SAAS,CAAC,OAAO,CAAC,CAAC;AAEtB,wBAAgB,eAAe,CAAC,SAAS,SAAS,WAAW,EAAE,EAC7D,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,SAAS,GAClB,SAAS,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC;AAsBjD,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAAC,OAAO,CAAC,CAAC;AAE1B,wBAAgB,mBAAmB,CAAC,SAAS,SAAS,WAAW,EAAE,EACjE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,SAAS,GAClB,aAAa,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAAC"}
@@ -2,8 +2,9 @@ export const INTERNAL_AUTH_HEADER = "x-sveltebase-sync-auth";
2
2
  function getEnv(options) {
3
3
  return options.platform?.env;
4
4
  }
5
- function normalizeEndpoint(baseUrl, pathname) {
6
- return new URL(pathname, baseUrl).toString();
5
+ function hasDevBroker() {
6
+ const globalObject = globalThis;
7
+ return Boolean(globalObject.__sveltebase_sync_dev_broker__);
7
8
  }
8
9
  async function publishToDurableObject(platform, durableObjectBinding, pathname, body) {
9
10
  const namespace = platform.env[durableObjectBinding];
@@ -21,44 +22,26 @@ async function publishToDurableObject(platform, durableObjectBinding, pathname,
21
22
  throw new Error(await response.text());
22
23
  }
23
24
  }
24
- async function publishToServiceBinding(binding, pathname, body) {
25
- const response = await binding.fetch(`https://sync.internal${pathname}`, {
26
- method: "POST",
27
- headers: { "Content-Type": "application/json" },
28
- body: JSON.stringify(body),
29
- });
30
- if (!response.ok) {
31
- throw new Error(await response.text());
32
- }
33
- }
34
- async function publishToFallbackUrl(fallbackUrl, pathname, body) {
35
- const response = await fetch(normalizeEndpoint(fallbackUrl, pathname), {
36
- method: "POST",
37
- headers: { "Content-Type": "application/json" },
38
- body: JSON.stringify(body),
39
- });
40
- if (!response.ok) {
41
- throw new Error(await response.text());
25
+ async function publishToDevBroker(pathname, body) {
26
+ const devEngine = await import("./dev-engine.js");
27
+ if (pathname === "/broadcast-batch") {
28
+ await devEngine.broadcastExternalBatchChange(String(body.channel), Array.isArray(body.changes) ? body.changes : []);
29
+ return;
42
30
  }
31
+ await devEngine.broadcastExternalChange(String(body.channel), body.action, body.key, body.data);
43
32
  }
44
33
  async function publish(options, pathname, body) {
45
34
  const env = getEnv(options);
46
35
  const durableObjectBinding = options.durableObjectBinding ?? "SYNC_ENGINE";
47
- const bindingName = options.binding ?? "SYNC_WORKER";
48
36
  if (options.platform && env?.[durableObjectBinding]) {
49
37
  await publishToDurableObject(options.platform, durableObjectBinding, pathname, body);
50
38
  return;
51
39
  }
52
- const serviceBinding = env?.[bindingName];
53
- if (serviceBinding?.fetch) {
54
- await publishToServiceBinding(serviceBinding, pathname, body);
55
- return;
56
- }
57
- if (options.fallbackUrl) {
58
- await publishToFallbackUrl(options.fallbackUrl, pathname, body);
40
+ if (hasDevBroker()) {
41
+ await publishToDevBroker(pathname, body);
59
42
  return;
60
43
  }
61
- throw new Error(`Missing sync publisher target: provide platform.env.${durableObjectBinding}, platform.env.${bindingName}, or fallbackUrl`);
44
+ throw new Error(`Missing sync publisher target: provide platform.env.${durableObjectBinding} or run vite dev with syncDevPlugin()`);
62
45
  }
63
46
  export function createPublisher(options, handlers) {
64
47
  void handlers;
@@ -1,10 +1,7 @@
1
1
  import type { RequestHandler } from "@sveltejs/kit";
2
- export type SyncProxyOptions = {
3
- binding?: string;
4
- fallbackUrl?: string;
5
- };
6
- export declare function syncProxy(options?: SyncProxyOptions): {
2
+ import { type SyncWorkerOptions } from "../cloudflare/handler.js";
3
+ export type SyncEngineRouteOptions<TAuth = unknown> = SyncWorkerOptions<TAuth>;
4
+ export declare function syncEngineRoute<TAuth = unknown>(options: SyncEngineRouteOptions<TAuth>): {
7
5
  GET: RequestHandler;
8
- POST: RequestHandler;
9
6
  };
10
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sveltekit/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAeF,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG;IACrD,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,EAAE,cAAc,CAAC;CACtB,CA6BA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sveltekit/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,OAAO,IAChD,iBAAiB,CAAC,KAAK,CAAC,CAAC;AAkB3B,wBAAgB,eAAe,CAAC,KAAK,GAAG,OAAO,EAC7C,OAAO,EAAE,sBAAsB,CAAC,KAAK,CAAC,GACrC;IACD,GAAG,EAAE,cAAc,CAAC;CACrB,CAuBA"}
@@ -1,29 +1,23 @@
1
- function buildFallbackRequest(request, fallbackUrl) {
2
- const sourceUrl = new URL(request.url);
3
- const targetUrl = new URL(fallbackUrl);
4
- targetUrl.search = sourceUrl.search;
5
- return new Request(targetUrl, {
6
- method: request.method,
7
- headers: request.headers,
8
- body: request.body,
9
- redirect: request.redirect,
10
- });
1
+ import { handleSyncRequest, } from "../cloudflare/handler.js";
2
+ function getExecutionContext(platform) {
3
+ const context = platform?.context ?? platform?.ctx;
4
+ if (context)
5
+ return context;
6
+ return {
7
+ waitUntil() { },
8
+ passThroughOnException() { },
9
+ };
11
10
  }
12
- export function syncProxy(options) {
13
- const bindingName = options?.binding ?? "SYNC_WORKER";
11
+ export function syncEngineRoute(options) {
14
12
  const handler = async (event) => {
15
13
  const platform = event.platform;
16
- const serviceBinding = platform?.env?.[bindingName];
17
- if (serviceBinding?.fetch) {
18
- return serviceBinding.fetch(event.request);
19
- }
20
- if (options?.fallbackUrl) {
21
- return fetch(buildFallbackRequest(event.request, options.fallbackUrl));
14
+ const env = platform?.env;
15
+ if (!env) {
16
+ return new Response("Missing Cloudflare platform env. Configure adapter-cloudflare platformProxy for Vite dev or run under wrangler.", { status: 500 });
22
17
  }
23
- return new Response(`Missing sync Worker binding ${bindingName}. Configure a service binding or pass fallbackUrl to syncProxy().`, { status: 500 });
18
+ return handleSyncRequest(event.request, env, getExecutionContext(platform), options);
24
19
  };
25
20
  return {
26
21
  GET: handler,
27
- POST: handler,
28
22
  };
29
23
  }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "vite";
2
+ import type { SyncDevAuthOptions } from "./server/dev-engine.js";
3
+ export type SyncDevPluginOptions<TAuth = unknown> = SyncDevAuthOptions<TAuth> & {
4
+ handlersPath?: string;
5
+ path?: string;
6
+ };
7
+ export declare function syncDevPlugin<TAuth = unknown>(options?: SyncDevPluginOptions<TAuth>): Plugin;
8
+ //# sourceMappingURL=vite.d.ts.map
@@ -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;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAajE,MAAM,MAAM,oBAAoB,CAAC,KAAK,GAAG,OAAO,IAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAgB,aAAa,CAAC,KAAK,GAAG,OAAO,EAC3C,OAAO,CAAC,EAAE,oBAAoB,CAAC,KAAK,CAAC,GACpC,MAAM,CA6ER"}
package/dist/vite.js ADDED
@@ -0,0 +1,56 @@
1
+ const DEFAULT_SYNC_PATH = "/api/sync";
2
+ export function syncDevPlugin(options) {
3
+ const handlersPath = options?.handlersPath ?? "/src/lib/server/sync-handlers.ts";
4
+ const syncPath = options?.path ?? DEFAULT_SYNC_PATH;
5
+ return {
6
+ name: "sveltebase-sync-dev-websocket",
7
+ apply: "serve",
8
+ async configureServer(server) {
9
+ const { WebSocketServer } = (await import("ws"));
10
+ const wss = new WebSocketServer({ noServer: true });
11
+ server.httpServer?.on("upgrade", (request, socket, head) => {
12
+ const url = new URL(request.url ?? "", `http://${request.headers.host ?? "localhost"}`);
13
+ if (url.pathname !== syncPath) {
14
+ return;
15
+ }
16
+ wss.handleUpgrade(request, socket, head, (client) => {
17
+ const messageQueue = [];
18
+ const onMessage = (data) => {
19
+ messageQueue.push(data);
20
+ };
21
+ client.on("message", onMessage);
22
+ void (async () => {
23
+ try {
24
+ const handlersModule = await server.ssrLoadModule(handlersPath);
25
+ const devEngine = await server.ssrLoadModule("@sveltebase/sync/server/dev-engine");
26
+ devEngine.setHandlers(handlersModule.handlers);
27
+ client.off("message", onMessage);
28
+ const connected = await devEngine.addClient(client, request, {
29
+ auth: options?.auth,
30
+ identity: options?.identity,
31
+ allowUnauthenticated: options?.allowUnauthenticated,
32
+ platform: options?.platform,
33
+ wranglerConfigPath: options?.wranglerConfigPath,
34
+ });
35
+ if (!connected)
36
+ return;
37
+ for (const message of messageQueue) {
38
+ client.emit("message", message);
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.error("sync dev plugin: websocket upgrade failed", err);
43
+ try {
44
+ client.off("message", onMessage);
45
+ client.close(1011, "Internal server error");
46
+ }
47
+ catch {
48
+ // Ignore close errors.
49
+ }
50
+ }
51
+ })();
52
+ });
53
+ });
54
+ },
55
+ };
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltebase/sync",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/server/index.d.ts",
26
26
  "default": "./dist/server/index.js"
27
27
  },
28
+ "./server/dev-engine": {
29
+ "types": "./dist/server/dev-engine.d.ts",
30
+ "default": "./dist/server/dev-engine.js"
31
+ },
28
32
  "./sveltekit": {
29
33
  "types": "./dist/sveltekit/index.d.ts",
30
34
  "default": "./dist/sveltekit/index.js"
@@ -32,10 +36,15 @@
32
36
  "./cloudflare": {
33
37
  "types": "./dist/cloudflare/index.d.ts",
34
38
  "default": "./dist/cloudflare/index.js"
39
+ },
40
+ "./vite": {
41
+ "types": "./dist/vite.d.ts",
42
+ "default": "./dist/vite.js"
35
43
  }
36
44
  },
37
45
  "dependencies": {
38
- "dexie": "^4.0.8"
46
+ "dexie": "^4.0.8",
47
+ "ws": "^8.18.3"
39
48
  },
40
49
  "peerDependencies": {
41
50
  "svelte": "^5.0.0",