create-syncular-app 0.58.0 → 0.60.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-syncular-app",
3
- "version": "0.58.0",
3
+ "version": "0.60.0",
4
4
  "author": "Benjamin Kniffler",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,6 +9,7 @@
9
9
  * clients`). Both talk real HTTP to http://localhost:8787.
10
10
  */
11
11
  import { makeClient } from './make-client';
12
+ import { todosInListSubscription } from './syncular.generated';
12
13
 
13
14
  const BASE_URL = process.env.SERVER_URL ?? 'http://localhost:8787';
14
15
  const LIST_ID = 'groceries';
@@ -18,10 +19,11 @@ const b = makeClient(BASE_URL, 'client-b');
18
19
  await a.start();
19
20
  await b.start();
20
21
 
21
- // Both clients subscribe to the same list (the requested scope).
22
- const sub = { id: 'todos', table: 'todos', scopes: { list_id: [LIST_ID] } };
23
- a.subscribe(sub);
24
- b.subscribe(sub);
22
+ // Both clients subscribe to the list's unit: one scope map covers every table
23
+ // scoped by `list_id`.
24
+ const list = todosInListSubscription.scopes({ listId: LIST_ID });
25
+ a.subscribe(list);
26
+ b.subscribe(list);
25
27
 
26
28
  // A writes a todo. mutate() records it locally + queues it for the next push.
27
29
  a.mutate([
@@ -13,7 +13,7 @@ import {
13
13
  } from '@syncular/server';
14
14
  import { createSyncularHono } from '@syncular/server-hono';
15
15
  import { makeClient } from './make-client';
16
- import { schema } from './syncular.generated';
16
+ import { schema, todosInListSubscription } from './syncular.generated';
17
17
 
18
18
  let server: ReturnType<typeof Bun.serve>;
19
19
  let baseUrl: string;
@@ -43,13 +43,9 @@ test('two clients converge through the server', async () => {
43
43
  await a.start();
44
44
  await b.start();
45
45
 
46
- const sub = {
47
- id: 'todos',
48
- table: 'todos',
49
- scopes: { list_id: ['groceries'] },
50
- };
51
- a.subscribe(sub);
52
- b.subscribe(sub);
46
+ const list = todosInListSubscription.scopes({ listId: 'groceries' });
47
+ a.subscribe(list);
48
+ b.subscribe(list);
53
49
 
54
50
  a.mutate([
55
51
  {
@@ -1,6 +1,6 @@
1
1
  // Generated by @syncular/typegen — DO NOT EDIT.
2
2
  // irVersion: 1
3
- // irHash: sha256:87f3b0e11fc0434b220472a00110771ac69dc0d56068d8ce69d0c1eb2930894e
3
+ // irHash: sha256:7a54dda11222e7937ece27856682da95d651ebc94cb95f9db77748f3282ffba2
4
4
 
5
5
  /** Structural descriptor consumed by renderer bindings; phantom type
6
6
  * fields make row/insert/update/id inference available without imports. */
@@ -18,24 +18,51 @@ export interface SyncTable<Row, Insert, Update, Id> {
18
18
 
19
19
  /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
20
20
  export const schema = {
21
- version: 1,
22
- tables: [
21
+ "version": 1,
22
+ "tables": [
23
23
  {
24
- name: 'todos',
25
- columns: [
26
- { name: 'id', type: 'string', nullable: false },
27
- { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'title', type: 'string', nullable: false },
29
- { name: 'done', type: 'boolean', nullable: false },
30
- { name: 'position', type: 'integer', nullable: false },
31
- { name: 'updated_at_ms', type: 'integer', nullable: false },
24
+ "name": "todos",
25
+ "primaryKey": "id",
26
+ "columns": [
27
+ {
28
+ "name": "id",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "list_id",
34
+ "type": "string",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "title",
39
+ "type": "string",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "done",
44
+ "type": "boolean",
45
+ "nullable": false
46
+ },
47
+ {
48
+ "name": "position",
49
+ "type": "integer",
50
+ "nullable": false
51
+ },
52
+ {
53
+ "name": "updated_at_ms",
54
+ "type": "integer",
55
+ "nullable": false
56
+ }
32
57
  ],
33
- primaryKey: 'id',
34
- scopes: [
35
- { pattern: 'list:{list_id}', column: 'list_id' },
36
- ],
37
- },
38
- ],
58
+ "scopes": [
59
+ {
60
+ "pattern": "list:{list_id}",
61
+ "column": "list_id"
62
+ }
63
+ ]
64
+ }
65
+ ]
39
66
  } as const;
40
67
 
41
68
  /** One todos row (§2.4 column order). */
@@ -79,11 +106,10 @@ export interface TodosInListParams {
79
106
  listId: string;
80
107
  }
81
108
 
82
- /** Requested-scope template for the 'todosInList' subscription. */
109
+ /** Unit template for the 'todosInList' subscription (SPEC §4.1). */
83
110
  export const todosInListSubscription = {
84
111
  name: 'todosInList',
85
- table: 'todos',
86
- scopes: (params: TodosInListParams): Record<string, string[]> => ({
87
- list_id: [params.listId],
112
+ scopes: (params: TodosInListParams): Record<string, string> => ({
113
+ list_id: params.listId,
88
114
  }),
89
115
  } as const;
@@ -58,16 +58,13 @@
58
58
  "subscriptions": [
59
59
  {
60
60
  "name": "todosInList",
61
- "table": "todos",
62
61
  "scopes": [
63
62
  {
64
63
  "variable": "list_id",
65
- "values": [
66
- {
67
- "kind": "parameter",
68
- "name": "listId"
69
- }
70
- ]
64
+ "value": {
65
+ "kind": "parameter",
66
+ "name": "listId"
67
+ }
71
68
  }
72
69
  ]
73
70
  }
@@ -8,10 +8,6 @@
8
8
  "schemaVersions": [{ "version": 1, "through": "0001_initial" }],
9
9
  "tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
10
10
  "subscriptions": [
11
- {
12
- "name": "todosInList",
13
- "table": "todos",
14
- "scopes": { "list_id": ["{listId}"] }
15
- }
11
+ { "name": "todosInList", "scopes": { "list_id": "{listId}" } }
16
12
  ]
17
13
  }
@@ -1,9 +1,10 @@
1
1
  {
2
- "formatVersion": 2,
2
+ "formatVersion": 3,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
- "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa"
6
+ "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa",
7
+ "schemaVersion": 1
7
8
  }
8
9
  ],
9
10
  "head": {
@@ -83,9 +83,14 @@ On desktop, the plugin owns the database path and the transport — see
83
83
  table before using a child wildcard. See
84
84
  [Scopes & authorization](https://syncular.dev/concepts-scopes/).
85
85
  2. **`src/server.ts` → `authenticate`** — plug in your real session/token
86
- check; return `{ actorId, partition }` or `null` for a 401.
86
+ check; return `{ actorId, partition }` or `null` for a 401. `POST /sync`
87
+ and the `/realtime` upgrade both run it. A browser WebSocket sends cookies
88
+ but no custom headers, so authenticate the upgrade from a cookie or a
89
+ [short-lived ticket](https://syncular.dev/guide-auth/#realtime-tickets).
87
90
  3. **`src-tauri/src/lib.rs` → `base_url`** — point the native core at your
88
91
  deployed sync endpoint (rotating auth goes through `client.setHeaders`).
92
+ Replace `localhost:8787` in the `connect-src` of the window's CSP
93
+ (`src-tauri/tauri.conf.json`) with the same origin.
89
94
  4. **`src/frontend/main.tsx`** — the UI. Everything under `<SyncProvider>` is
90
95
  shared code; grow it into your app.
91
96
  5. **`migrations/` + `syncular.json`** — append migrations and add tables or
@@ -9,3 +9,6 @@ CREATE TABLE todos (
9
9
  position INTEGER NOT NULL,
10
10
  updated_at_ms INTEGER NOT NULL
11
11
  );
12
+
13
+ -- listTodos reads one list at a time; this index lets SQLite search it.
14
+ CREATE INDEX idx_todos_list ON todos (list_id);
@@ -30,8 +30,6 @@ import { buildSqliteImage, SqliteServerStorage } from '@syncular/server/sqlite';
30
30
  import { schema } from './syncular.generated';
31
31
 
32
32
  const PORT = Number(process.env.PORT ?? 8787);
33
- const PARTITION = 'demo';
34
- const ACTOR_ID = 'demo-user';
35
33
 
36
34
  // -- sync server ------------------------------------------------------------
37
35
 
@@ -57,11 +55,19 @@ const config: SyncServerConfig = {
57
55
  realtime: hub,
58
56
  };
59
57
  await ensureSyncServerReady(config);
60
- const hono = createSyncularHono({
61
- config,
62
- // Replace with your real auth: return { actorId, partition } or null (401).
63
- authenticate: async () => ({ actorId: ACTOR_ID, partition: PARTITION }),
64
- });
58
+ // §8.5: a client that hears nothing for twice this interval drops the socket.
59
+ setInterval(() => hub.heartbeat(), 30_000);
60
+
61
+ /** Replace with your real auth: return { actorId, partition } or null (401).
62
+ * POST /sync and the /realtime upgrade both run it. A browser WebSocket sends
63
+ * cookies but no custom headers, so authenticate the upgrade from a cookie or
64
+ * a query parameter. */
65
+ async function authenticate(
66
+ _request: Request,
67
+ ): Promise<{ actorId: string; partition: string } | null> {
68
+ return { actorId: 'demo-user', partition: 'demo' };
69
+ }
70
+ const hono = createSyncularHono({ config, authenticate });
65
71
 
66
72
  // -- frontend build + static assets ------------------------------------------
67
73
 
@@ -129,6 +135,8 @@ function staticResponse(body: string | Uint8Array, type: string): Response {
129
135
  // -- one process, one port ----------------------------------------------------
130
136
 
131
137
  interface SocketData {
138
+ actorId: string;
139
+ partition: string;
132
140
  clientId: string;
133
141
  session?: RealtimeSession;
134
142
  }
@@ -140,9 +148,15 @@ const server = Bun.serve<SocketData, never>({
140
148
  const path = url.pathname;
141
149
 
142
150
  if (path === '/realtime') {
151
+ const auth = await authenticate(request);
152
+ if (auth === null) {
153
+ return new Response('realtime requires authentication', {
154
+ status: 401,
155
+ });
156
+ }
143
157
  const clientId = url.searchParams.get('clientId') ?? crypto.randomUUID();
144
- if (bunServer.upgrade(request, { data: { clientId } })) {
145
- return undefined as unknown as Response;
158
+ if (bunServer.upgrade(request, { data: { ...auth, clientId } })) {
159
+ return;
146
160
  }
147
161
  return new Response('expected a websocket upgrade', { status: 400 });
148
162
  }
@@ -172,8 +186,8 @@ const server = Bun.serve<SocketData, never>({
172
186
  open(ws) {
173
187
  hub
174
188
  .connect({
175
- partition: PARTITION,
176
- actorId: ACTOR_ID,
189
+ partition: ws.data.partition,
190
+ actorId: ws.data.actorId,
177
191
  clientId: ws.data.clientId,
178
192
  send: (data) => {
179
193
  ws.send(data);
@@ -63,9 +63,9 @@ test('two clients converge through the scaffolded server config', async () => {
63
63
  await a.start();
64
64
  await b.start();
65
65
 
66
- const scopes = todoListSubscription.scopes({ listId: 'welcome' });
67
- a.subscribe({ id: 'todos', table: 'todos', scopes });
68
- b.subscribe({ id: 'todos', table: 'todos', scopes });
66
+ const list = todoListSubscription.scopes({ listId: 'welcome' });
67
+ a.subscribe(list);
68
+ b.subscribe(list);
69
69
 
70
70
  a.mutate([
71
71
  {
@@ -1,6 +1,6 @@
1
1
  // Generated by @syncular/typegen — DO NOT EDIT.
2
2
  // irVersion: 1
3
- // irHash: sha256:2ffc5b70cb96cd546326de9c06868bd988c9fa3459fb3bac8428a81dcc85fca0
3
+ // irHash: sha256:6bcec8061116d46f6e483f2f338f9d706972d272572c67b510e54a78e6b966c8
4
4
 
5
5
  /** Structural descriptor consumed by renderer bindings; phantom type
6
6
  * fields make row/insert/update/id inference available without imports. */
@@ -18,24 +18,60 @@ export interface SyncTable<Row, Insert, Update, Id> {
18
18
 
19
19
  /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
20
20
  export const schema = {
21
- version: 1,
22
- tables: [
21
+ "version": 1,
22
+ "tables": [
23
23
  {
24
- name: 'todos',
25
- columns: [
26
- { name: 'id', type: 'string', nullable: false },
27
- { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'title', type: 'string', nullable: false },
29
- { name: 'done', type: 'boolean', nullable: false },
30
- { name: 'position', type: 'integer', nullable: false },
31
- { name: 'updated_at_ms', type: 'integer', nullable: false },
24
+ "name": "todos",
25
+ "primaryKey": "id",
26
+ "columns": [
27
+ {
28
+ "name": "id",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "list_id",
34
+ "type": "string",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "title",
39
+ "type": "string",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "done",
44
+ "type": "boolean",
45
+ "nullable": false
46
+ },
47
+ {
48
+ "name": "position",
49
+ "type": "integer",
50
+ "nullable": false
51
+ },
52
+ {
53
+ "name": "updated_at_ms",
54
+ "type": "integer",
55
+ "nullable": false
56
+ }
32
57
  ],
33
- primaryKey: 'id',
34
- scopes: [
35
- { pattern: 'list:{list_id}', column: 'list_id' },
58
+ "scopes": [
59
+ {
60
+ "pattern": "list:{list_id}",
61
+ "column": "list_id"
62
+ }
36
63
  ],
37
- },
38
- ],
64
+ "indexes": [
65
+ {
66
+ "name": "idx_todos_list",
67
+ "columns": [
68
+ "list_id"
69
+ ],
70
+ "unique": false
71
+ }
72
+ ]
73
+ }
74
+ ]
39
75
  } as const;
40
76
 
41
77
  /** One todos row (§2.4 column order). */
@@ -79,11 +115,10 @@ export interface TodoListParams {
79
115
  listId: string;
80
116
  }
81
117
 
82
- /** Requested-scope template for the 'todoList' subscription. */
118
+ /** Unit template for the 'todoList' subscription (SPEC §4.1). */
83
119
  export const todoListSubscription = {
84
120
  name: 'todoList',
85
- table: 'todos',
86
- scopes: (params: TodoListParams): Record<string, string[]> => ({
87
- list_id: [params.listId],
121
+ scopes: (params: TodoListParams): Record<string, string> => ({
122
+ list_id: params.listId,
88
123
  }),
89
124
  } as const;
@@ -98,12 +98,11 @@ export interface QueryResultColumn {
98
98
  readonly nullable: boolean;
99
99
  }
100
100
 
101
+ /** A window base (SPEC §4.8) and the unit values a query reads. */
101
102
  export interface WindowCoverage {
102
103
  readonly base: {
103
- readonly table: string;
104
104
  readonly variable: string;
105
- readonly fixedScopes?: Readonly<Record<string, readonly string[]>>;
106
- readonly params?: string;
105
+ readonly fixedScopes?: Readonly<Record<string, string>>;
107
106
  };
108
107
  readonly units: readonly string[];
109
108
  }
@@ -21,7 +21,7 @@ tauri = { version = "2", features = [] }
21
21
  # The syncular plugin from crates.io. `native-transport` gives the core its
22
22
  # real HTTP + WebSocket transport (ureq + tungstenite) inside the host
23
23
  # process — the webview never talks to the sync server directly.
24
- tauri-plugin-syncular = { version = "0.58.0", features = ["native-transport"] }
24
+ tauri-plugin-syncular = { version = "0.60.0", features = ["native-transport"] }
25
25
 
26
26
  [features]
27
27
  default = []
@@ -17,7 +17,7 @@
17
17
  }
18
18
  ],
19
19
  "security": {
20
- "csp": null,
20
+ "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://localhost:8787 ws://localhost:8787",
21
21
  "capabilities": ["syncular"]
22
22
  }
23
23
  },
@@ -52,22 +52,28 @@
52
52
  "column": "list_id"
53
53
  }
54
54
  ],
55
+ "indexes": [
56
+ {
57
+ "name": "idx_todos_list",
58
+ "columns": [
59
+ "list_id"
60
+ ],
61
+ "unique": false
62
+ }
63
+ ],
55
64
  "extensions": {}
56
65
  }
57
66
  ],
58
67
  "subscriptions": [
59
68
  {
60
69
  "name": "todoList",
61
- "table": "todos",
62
70
  "scopes": [
63
71
  {
64
72
  "variable": "list_id",
65
- "values": [
66
- {
67
- "kind": "parameter",
68
- "name": "listId"
69
- }
70
- ]
73
+ "value": {
74
+ "kind": "parameter",
75
+ "name": "listId"
76
+ }
71
77
  }
72
78
  ]
73
79
  }
@@ -10,10 +10,6 @@
10
10
  "schemaVersions": [{ "version": 1, "through": "0001_initial" }],
11
11
  "tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
12
12
  "subscriptions": [
13
- {
14
- "name": "todoList",
15
- "table": "todos",
16
- "scopes": { "list_id": ["{listId}"] }
17
- }
13
+ { "name": "todoList", "scopes": { "list_id": "{listId}" } }
18
14
  ]
19
15
  }
@@ -1,47 +1,50 @@
1
1
  {
2
- "formatVersion": 1,
2
+ "formatVersion": 3,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
- "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa",
7
- "tables": [
8
- {
9
- "name": "todos",
10
- "primaryKey": "id",
11
- "columns": [
12
- {
13
- "name": "id",
14
- "type": "string",
15
- "nullable": false
16
- },
17
- {
18
- "name": "list_id",
19
- "type": "string",
20
- "nullable": false
21
- },
22
- {
23
- "name": "title",
24
- "type": "string",
25
- "nullable": false
26
- },
27
- {
28
- "name": "done",
29
- "type": "boolean",
30
- "nullable": false
31
- },
32
- {
33
- "name": "position",
34
- "type": "integer",
35
- "nullable": false
36
- },
37
- {
38
- "name": "updated_at_ms",
39
- "type": "integer",
40
- "nullable": false
41
- }
42
- ]
43
- }
44
- ]
6
+ "sha256": "33fdb7df2e4dc4c598137acdacf009922ea1d107b2c4284efe62ec25eb681116",
7
+ "schemaVersion": 1
45
8
  }
46
- ]
9
+ ],
10
+ "head": {
11
+ "tables": [
12
+ {
13
+ "name": "todos",
14
+ "primaryKey": "id",
15
+ "columns": [
16
+ {
17
+ "name": "id",
18
+ "type": "string",
19
+ "nullable": false
20
+ },
21
+ {
22
+ "name": "list_id",
23
+ "type": "string",
24
+ "nullable": false
25
+ },
26
+ {
27
+ "name": "title",
28
+ "type": "string",
29
+ "nullable": false
30
+ },
31
+ {
32
+ "name": "done",
33
+ "type": "boolean",
34
+ "nullable": false
35
+ },
36
+ {
37
+ "name": "position",
38
+ "type": "integer",
39
+ "nullable": false
40
+ },
41
+ {
42
+ "name": "updated_at_ms",
43
+ "type": "integer",
44
+ "nullable": false
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
47
50
  }
@@ -54,7 +54,10 @@ covered by `tsc`).
54
54
  See
55
55
  [Scopes & authorization](https://syncular.dev/concepts-scopes/).
56
56
  2. **`src/server.ts` → `authenticate`** — plug in your real session/token
57
- check; return `{ actorId, partition }` or `null` for a 401.
57
+ check; return `{ actorId, partition }` or `null` for a 401. `POST /sync`
58
+ and the `/realtime` upgrade both run it. A browser WebSocket sends cookies
59
+ but no custom headers, so authenticate the upgrade from a cookie or a
60
+ [short-lived ticket](https://syncular.dev/guide-auth/#realtime-tickets).
58
61
  3. **`src/frontend/main.ts`** — the UI. It queries and mutates through the
59
62
  worker handle; grow it into your app.
60
63
  4. **`migrations/` + `syncular.json`** — append migrations and add tables or
@@ -72,11 +72,8 @@ async function main(): Promise<void> {
72
72
  onSynced: () => void refresh(),
73
73
  });
74
74
 
75
- await handle.subscribe({
76
- id: 'todos',
77
- table: 'todos',
78
- scopes: todoListSubscription.scopes({ listId: LIST_ID }),
79
- });
75
+ // One unit per list: the scope map covers every table scoped by `list_id`.
76
+ await handle.subscribe(todoListSubscription.scopes({ listId: LIST_ID }));
80
77
 
81
78
  // The supported host policy owns connect + catch-up, retries transient
82
79
  // startup/socket loss, and suspends while this page is offline.
@@ -26,8 +26,6 @@ import { buildSqliteImage, SqliteServerStorage } from '@syncular/server/sqlite';
26
26
  import { schema } from './syncular.generated';
27
27
 
28
28
  const PORT = Number(process.env.PORT ?? 8787);
29
- const PARTITION = 'demo';
30
- const ACTOR_ID = 'demo-user';
31
29
 
32
30
  // -- sync server ------------------------------------------------------------
33
31
 
@@ -53,11 +51,19 @@ const config: SyncServerConfig = {
53
51
  realtime: hub,
54
52
  };
55
53
  await ensureSyncServerReady(config);
56
- const hono = createSyncularHono({
57
- config,
58
- // Replace with your real auth: return { actorId, partition } or null (401).
59
- authenticate: async () => ({ actorId: ACTOR_ID, partition: PARTITION }),
60
- });
54
+ // §8.5: a client that hears nothing for twice this interval drops the socket.
55
+ setInterval(() => hub.heartbeat(), 30_000);
56
+
57
+ /** Replace with your real auth: return { actorId, partition } or null (401).
58
+ * POST /sync and the /realtime upgrade both run it. A browser WebSocket sends
59
+ * cookies but no custom headers, so authenticate the upgrade from a cookie or
60
+ * a query parameter. */
61
+ async function authenticate(
62
+ _request: Request,
63
+ ): Promise<{ actorId: string; partition: string } | null> {
64
+ return { actorId: 'demo-user', partition: 'demo' };
65
+ }
66
+ const hono = createSyncularHono({ config, authenticate });
61
67
 
62
68
  // -- frontend build + static assets ------------------------------------------
63
69
 
@@ -124,6 +130,8 @@ function staticResponse(body: string | Uint8Array, type: string): Response {
124
130
  // -- one process, one port ----------------------------------------------------
125
131
 
126
132
  interface SocketData {
133
+ actorId: string;
134
+ partition: string;
127
135
  clientId: string;
128
136
  session?: RealtimeSession;
129
137
  }
@@ -135,9 +143,15 @@ const server = Bun.serve<SocketData, never>({
135
143
  const path = url.pathname;
136
144
 
137
145
  if (path === '/realtime') {
146
+ const auth = await authenticate(request);
147
+ if (auth === null) {
148
+ return new Response('realtime requires authentication', {
149
+ status: 401,
150
+ });
151
+ }
138
152
  const clientId = url.searchParams.get('clientId') ?? crypto.randomUUID();
139
- if (bunServer.upgrade(request, { data: { clientId } })) {
140
- return undefined as unknown as Response;
153
+ if (bunServer.upgrade(request, { data: { ...auth, clientId } })) {
154
+ return;
141
155
  }
142
156
  return new Response('expected a websocket upgrade', { status: 400 });
143
157
  }
@@ -167,8 +181,8 @@ const server = Bun.serve<SocketData, never>({
167
181
  open(ws) {
168
182
  hub
169
183
  .connect({
170
- partition: PARTITION,
171
- actorId: ACTOR_ID,
184
+ partition: ws.data.partition,
185
+ actorId: ws.data.actorId,
172
186
  clientId: ws.data.clientId,
173
187
  send: (data) => {
174
188
  ws.send(data);
@@ -62,9 +62,9 @@ test('two clients converge through the scaffolded server config', async () => {
62
62
  await a.start();
63
63
  await b.start();
64
64
 
65
- const scopes = todoListSubscription.scopes({ listId: 'welcome' });
66
- a.subscribe({ id: 'todos', table: 'todos', scopes });
67
- b.subscribe({ id: 'todos', table: 'todos', scopes });
65
+ const list = todoListSubscription.scopes({ listId: 'welcome' });
66
+ a.subscribe(list);
67
+ b.subscribe(list);
68
68
 
69
69
  a.mutate([
70
70
  {
@@ -1,6 +1,6 @@
1
1
  // Generated by @syncular/typegen — DO NOT EDIT.
2
2
  // irVersion: 1
3
- // irHash: sha256:2ffc5b70cb96cd546326de9c06868bd988c9fa3459fb3bac8428a81dcc85fca0
3
+ // irHash: sha256:9c3077b1f537f8d84725f38d0b5234f7a53520777265f36f10059651d2cddd32
4
4
 
5
5
  /** Structural descriptor consumed by renderer bindings; phantom type
6
6
  * fields make row/insert/update/id inference available without imports. */
@@ -18,24 +18,51 @@ export interface SyncTable<Row, Insert, Update, Id> {
18
18
 
19
19
  /** ServerSchema-compatible schema object (SPEC §2.4, §3.1). */
20
20
  export const schema = {
21
- version: 1,
22
- tables: [
21
+ "version": 1,
22
+ "tables": [
23
23
  {
24
- name: 'todos',
25
- columns: [
26
- { name: 'id', type: 'string', nullable: false },
27
- { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'title', type: 'string', nullable: false },
29
- { name: 'done', type: 'boolean', nullable: false },
30
- { name: 'position', type: 'integer', nullable: false },
31
- { name: 'updated_at_ms', type: 'integer', nullable: false },
24
+ "name": "todos",
25
+ "primaryKey": "id",
26
+ "columns": [
27
+ {
28
+ "name": "id",
29
+ "type": "string",
30
+ "nullable": false
31
+ },
32
+ {
33
+ "name": "list_id",
34
+ "type": "string",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "title",
39
+ "type": "string",
40
+ "nullable": false
41
+ },
42
+ {
43
+ "name": "done",
44
+ "type": "boolean",
45
+ "nullable": false
46
+ },
47
+ {
48
+ "name": "position",
49
+ "type": "integer",
50
+ "nullable": false
51
+ },
52
+ {
53
+ "name": "updated_at_ms",
54
+ "type": "integer",
55
+ "nullable": false
56
+ }
32
57
  ],
33
- primaryKey: 'id',
34
- scopes: [
35
- { pattern: 'list:{list_id}', column: 'list_id' },
36
- ],
37
- },
38
- ],
58
+ "scopes": [
59
+ {
60
+ "pattern": "list:{list_id}",
61
+ "column": "list_id"
62
+ }
63
+ ]
64
+ }
65
+ ]
39
66
  } as const;
40
67
 
41
68
  /** One todos row (§2.4 column order). */
@@ -79,11 +106,10 @@ export interface TodoListParams {
79
106
  listId: string;
80
107
  }
81
108
 
82
- /** Requested-scope template for the 'todoList' subscription. */
109
+ /** Unit template for the 'todoList' subscription (SPEC §4.1). */
83
110
  export const todoListSubscription = {
84
111
  name: 'todoList',
85
- table: 'todos',
86
- scopes: (params: TodoListParams): Record<string, string[]> => ({
87
- list_id: [params.listId],
112
+ scopes: (params: TodoListParams): Record<string, string> => ({
113
+ list_id: params.listId,
88
114
  }),
89
115
  } as const;
@@ -58,16 +58,13 @@
58
58
  "subscriptions": [
59
59
  {
60
60
  "name": "todoList",
61
- "table": "todos",
62
61
  "scopes": [
63
62
  {
64
63
  "variable": "list_id",
65
- "values": [
66
- {
67
- "kind": "parameter",
68
- "name": "listId"
69
- }
70
- ]
64
+ "value": {
65
+ "kind": "parameter",
66
+ "name": "listId"
67
+ }
71
68
  }
72
69
  ]
73
70
  }
@@ -8,10 +8,6 @@
8
8
  "schemaVersions": [{ "version": 1, "through": "0001_initial" }],
9
9
  "tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
10
10
  "subscriptions": [
11
- {
12
- "name": "todoList",
13
- "table": "todos",
14
- "scopes": { "list_id": ["{listId}"] }
15
- }
11
+ { "name": "todoList", "scopes": { "list_id": "{listId}" } }
16
12
  ]
17
13
  }
@@ -1,47 +1,50 @@
1
1
  {
2
- "formatVersion": 1,
2
+ "formatVersion": 3,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
6
  "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa",
7
- "tables": [
8
- {
9
- "name": "todos",
10
- "primaryKey": "id",
11
- "columns": [
12
- {
13
- "name": "id",
14
- "type": "string",
15
- "nullable": false
16
- },
17
- {
18
- "name": "list_id",
19
- "type": "string",
20
- "nullable": false
21
- },
22
- {
23
- "name": "title",
24
- "type": "string",
25
- "nullable": false
26
- },
27
- {
28
- "name": "done",
29
- "type": "boolean",
30
- "nullable": false
31
- },
32
- {
33
- "name": "position",
34
- "type": "integer",
35
- "nullable": false
36
- },
37
- {
38
- "name": "updated_at_ms",
39
- "type": "integer",
40
- "nullable": false
41
- }
42
- ]
43
- }
44
- ]
7
+ "schemaVersion": 1
45
8
  }
46
- ]
9
+ ],
10
+ "head": {
11
+ "tables": [
12
+ {
13
+ "name": "todos",
14
+ "primaryKey": "id",
15
+ "columns": [
16
+ {
17
+ "name": "id",
18
+ "type": "string",
19
+ "nullable": false
20
+ },
21
+ {
22
+ "name": "list_id",
23
+ "type": "string",
24
+ "nullable": false
25
+ },
26
+ {
27
+ "name": "title",
28
+ "type": "string",
29
+ "nullable": false
30
+ },
31
+ {
32
+ "name": "done",
33
+ "type": "boolean",
34
+ "nullable": false
35
+ },
36
+ {
37
+ "name": "position",
38
+ "type": "integer",
39
+ "nullable": false
40
+ },
41
+ {
42
+ "name": "updated_at_ms",
43
+ "type": "integer",
44
+ "nullable": false
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
47
50
  }