create-syncular-app 0.15.47 → 0.16.1

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
@@ -13,7 +13,7 @@ bunx create-syncular-app my-app --template web
13
13
  |---|---|
14
14
  | `minimal` | Server + a terminal two-client convergence demo (no browser) — migrations + manifest + `generate` wiring. Copy-evolved from `examples/quickstart`. The smallest honest starting point. |
15
15
  | `web` | Hono server + WebSocket realtime + a single-pane browser todo app whose whole client core runs in a Web Worker on OPFS. Derived from `apps/demo`, slimmed to one pane (no conflict simulator, no blob attachments) — the minimal browser app a real user starts from. |
16
- | `tauri` | One React codebase, web + desktop: the `web` template's server plus a shared React tree behind the `__TAURI_INTERNALS__` engine seam (`src/frontend/engine.ts`) — worker core on OPFS in the browser, native Rust core in a `src-tauri/` host (`tauri-plugin-syncular` from crates.io, `native-transport`). Derived from `bindings/tauri/example` + the [web+desktop guide](../../apps/docs/src/content/guide-web-desktop.md). |
16
+ | `tauri` | One React codebase, web + desktop: the `web` template's server plus a shared React tree behind the `__TAURI_INTERNALS__` engine seam (`src/frontend/engine.ts`) — worker core on OPFS in the browser, native Rust core in a `src-tauri/` host (`tauri-plugin-syncular` from crates.io, `native-transport`). Derived from `bindings/tauri/example` + the [Tauri guide](../../apps/docs/src/content/platform-tauri.md). |
17
17
 
18
18
  Each template ships its own `README.md` (run steps, what to edit first),
19
19
  `.gitignore` (as `gitignore` — see below), a working `tsconfig.json`, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-syncular-app",
3
- "version": "0.15.47",
3
+ "version": "0.16.1",
4
4
  "author": "Benjamin Kniffler",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,9 +1,11 @@
1
1
  -- Your schema, one table to start. typegen reads this to derive the schema
2
2
  -- SHAPE (column types, primary key); the server manages its own sync_* tables
3
3
  -- and never runs this migration.
4
- CREATE TABLE notes (
4
+ CREATE TABLE todos (
5
5
  id TEXT PRIMARY KEY,
6
6
  list_id TEXT NOT NULL,
7
- body TEXT NOT NULL,
7
+ title TEXT NOT NULL,
8
+ done BOOLEAN NOT NULL,
9
+ position INTEGER NOT NULL,
8
10
  updated_at_ms INTEGER NOT NULL
9
11
  );
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Two clients, one server, terminal-visible convergence.
3
3
  *
4
- * A writes a note; B a completely independent client core with its own local
5
- * database bootstraps, syncs, and reads the same row back. This is the proof
4
+ * A writes a todo; B (a completely independent client core with its own local
5
+ * database) bootstraps, syncs, and reads the same row back. This is the proof
6
6
  * that sync works end to end, no browser required.
7
7
  *
8
8
  * Run the server first (`bun run server`), then this script (`bun run
@@ -11,7 +11,7 @@
11
11
  import { makeClient } from './make-client';
12
12
 
13
13
  const BASE_URL = process.env.SERVER_URL ?? 'http://localhost:8787';
14
- const LIST_ID = 'welcome';
14
+ const LIST_ID = 'groceries';
15
15
 
16
16
  const a = makeClient(BASE_URL, 'client-a');
17
17
  const b = makeClient(BASE_URL, 'client-b');
@@ -19,34 +19,35 @@ await a.start();
19
19
  await b.start();
20
20
 
21
21
  // Both clients subscribe to the same list (the requested scope).
22
- const sub = { id: 'notes', table: 'notes', scopes: { list_id: [LIST_ID] } };
22
+ const sub = { id: 'todos', table: 'todos', scopes: { list_id: [LIST_ID] } };
23
23
  a.subscribe(sub);
24
24
  b.subscribe(sub);
25
25
 
26
- // A writes a note. mutate() records it locally + queues it for the next push.
27
- const now = Date.now();
26
+ // A writes a todo. mutate() records it locally + queues it for the next push.
28
27
  a.mutate([
29
28
  {
30
- table: 'notes',
29
+ table: 'todos',
31
30
  op: 'upsert',
32
31
  values: {
33
- id: 'note-1',
32
+ id: 'todo-1',
34
33
  list_id: LIST_ID,
35
- body: 'Hello from client A',
36
- updated_at_ms: now,
34
+ title: 'Buy milk',
35
+ done: false,
36
+ position: 1,
37
+ updated_at_ms: Date.now(),
37
38
  },
38
39
  },
39
40
  ]);
40
- console.log('A: wrote note-1, pushing…');
41
+ console.log('A: wrote todo-1, pushing…');
41
42
  await a.syncUntilIdle(); // push A's outbox to the server
42
43
 
43
44
  console.log('B: syncing…');
44
- await b.syncUntilIdle(); // B bootstraps the list and applies A's note
45
+ await b.syncUntilIdle(); // B bootstraps the list and applies A's todo
45
46
 
46
- const rows = b.query('SELECT id, body FROM notes ORDER BY id');
47
+ const rows = b.query('SELECT id, title FROM todos ORDER BY id');
47
48
  console.log('B sees:', rows);
48
49
 
49
- const converged = rows.length === 1 && rows[0]?.body === 'Hello from client A';
50
+ const converged = rows.length === 1 && rows[0]?.title === 'Buy milk';
50
51
  console.log(converged ? '\n✓ converged' : '\n✗ did NOT converge');
51
52
 
52
53
  await a.close();
@@ -43,18 +43,24 @@ test('two clients converge through the server', async () => {
43
43
  await a.start();
44
44
  await b.start();
45
45
 
46
- const sub = { id: 'notes', table: 'notes', scopes: { list_id: ['welcome'] } };
46
+ const sub = {
47
+ id: 'todos',
48
+ table: 'todos',
49
+ scopes: { list_id: ['groceries'] },
50
+ };
47
51
  a.subscribe(sub);
48
52
  b.subscribe(sub);
49
53
 
50
54
  a.mutate([
51
55
  {
52
- table: 'notes',
56
+ table: 'todos',
53
57
  op: 'upsert',
54
58
  values: {
55
- id: 'note-1',
56
- list_id: 'welcome',
57
- body: 'Hello from client A',
59
+ id: 'todo-1',
60
+ list_id: 'groceries',
61
+ title: 'Buy milk',
62
+ done: false,
63
+ position: 1,
58
64
  updated_at_ms: Date.now(),
59
65
  },
60
66
  },
@@ -62,8 +68,8 @@ test('two clients converge through the server', async () => {
62
68
  await a.syncUntilIdle();
63
69
  await b.syncUntilIdle();
64
70
 
65
- const rows = b.query('SELECT id, body FROM notes ORDER BY id');
66
- expect(rows).toEqual([{ id: 'note-1', body: 'Hello from client A' }]);
71
+ const rows = b.query('SELECT id, title FROM todos ORDER BY id');
72
+ expect(rows).toEqual([{ id: 'todo-1', title: 'Buy milk' }]);
67
73
 
68
74
  await a.close();
69
75
  await b.close();
@@ -1,6 +1,6 @@
1
1
  // Generated by @syncular/typegen — DO NOT EDIT.
2
2
  // irVersion: 1
3
- // irHash: sha256:e2e39c94ce6c13720e1f1579e2523d460fd6bb3fea2f8eaaa114305b7aa332ba
3
+ // irHash: sha256:87f3b0e11fc0434b220472a00110771ac69dc0d56068d8ce69d0c1eb2930894e
4
4
 
5
5
  /** Structural descriptor consumed by renderer bindings; phantom type
6
6
  * fields make row/insert/update/id inference available without imports. */
@@ -21,11 +21,13 @@ export const schema = {
21
21
  version: 1,
22
22
  tables: [
23
23
  {
24
- name: 'notes',
24
+ name: 'todos',
25
25
  columns: [
26
26
  { name: 'id', type: 'string', nullable: false },
27
27
  { name: 'list_id', type: 'string', nullable: false },
28
- { name: 'body', 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 },
29
31
  { name: 'updated_at_ms', type: 'integer', nullable: false },
30
32
  ],
31
33
  primaryKey: 'id',
@@ -36,46 +38,52 @@ export const schema = {
36
38
  ],
37
39
  } as const;
38
40
 
39
- /** One notes row (§2.4 column order). */
40
- export interface NotesRow {
41
+ /** One todos row (§2.4 column order). */
42
+ export interface TodosRow {
41
43
  id: string;
42
44
  listId: string;
43
- body: string;
45
+ title: string;
46
+ done: boolean;
47
+ position: number;
44
48
  updatedAtMs: number;
45
49
  }
46
50
 
47
51
  /** Insert shape: nullable columns may be omitted. */
48
- export interface NotesInsert {
52
+ export interface TodosInsert {
49
53
  id: string;
50
54
  listId: string;
51
- body: string;
55
+ title: string;
56
+ done: boolean;
57
+ position: number;
52
58
  updatedAtMs: number;
53
59
  }
54
60
 
55
61
  /** Update shape: primary key required, all other columns optional. */
56
- export interface NotesUpdate {
62
+ export interface TodosUpdate {
57
63
  id: string;
58
64
  listId?: string;
59
- body?: string;
65
+ title?: string;
66
+ done?: boolean;
67
+ position?: number;
60
68
  updatedAtMs?: number;
61
69
  }
62
70
 
63
- /** Typed mutation/resource descriptor for 'notes'. */
64
- export const notesTable: SyncTable<NotesRow, NotesInsert, NotesUpdate, string> = {
65
- name: 'notes',
71
+ /** Typed mutation/resource descriptor for 'todos'. */
72
+ export const todosTable: SyncTable<TodosRow, TodosInsert, TodosUpdate, string> = {
73
+ name: 'todos',
66
74
  primaryKey: 'id',
67
75
  physicalPrimaryKey: 'id',
68
76
  };
69
77
 
70
- export interface NotesInListParams {
78
+ export interface TodosInListParams {
71
79
  listId: string;
72
80
  }
73
81
 
74
- /** Requested-scope template for the 'notesInList' subscription. */
75
- export const notesInListSubscription = {
76
- name: 'notesInList',
77
- table: 'notes',
78
- scopes: (params: NotesInListParams): Record<string, string[]> => ({
82
+ /** Requested-scope template for the 'todosInList' subscription. */
83
+ export const todosInListSubscription = {
84
+ name: 'todosInList',
85
+ table: 'todos',
86
+ scopes: (params: TodosInListParams): Record<string, string[]> => ({
79
87
  list_id: [params.listId],
80
88
  }),
81
89
  } as const;
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "tables": [
13
13
  {
14
- "name": "notes",
14
+ "name": "todos",
15
15
  "primaryKey": "id",
16
16
  "columns": [
17
17
  {
@@ -25,10 +25,20 @@
25
25
  "nullable": false
26
26
  },
27
27
  {
28
- "name": "body",
28
+ "name": "title",
29
29
  "type": "string",
30
30
  "nullable": false
31
31
  },
32
+ {
33
+ "name": "done",
34
+ "type": "boolean",
35
+ "nullable": false
36
+ },
37
+ {
38
+ "name": "position",
39
+ "type": "integer",
40
+ "nullable": false
41
+ },
32
42
  {
33
43
  "name": "updated_at_ms",
34
44
  "type": "integer",
@@ -47,8 +57,8 @@
47
57
  ],
48
58
  "subscriptions": [
49
59
  {
50
- "name": "notesInList",
51
- "table": "notes",
60
+ "name": "todosInList",
61
+ "table": "todos",
52
62
  "scopes": [
53
63
  {
54
64
  "variable": "list_id",
@@ -6,11 +6,11 @@
6
6
  "module": "./src/syncular.generated.ts"
7
7
  },
8
8
  "schemaVersions": [{ "version": 1, "through": "0001_initial" }],
9
- "tables": [{ "name": "notes", "scopes": ["list:{list_id}"] }],
9
+ "tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
10
10
  "subscriptions": [
11
11
  {
12
- "name": "notesInList",
13
- "table": "notes",
12
+ "name": "todosInList",
13
+ "table": "todos",
14
14
  "scopes": { "list_id": ["{listId}"] }
15
15
  }
16
16
  ]
@@ -1,37 +1,49 @@
1
1
  {
2
- "formatVersion": 1,
2
+ "formatVersion": 2,
3
3
  "migrations": [
4
4
  {
5
5
  "name": "0001_initial",
6
- "sha256": "355c1e3c68757f31a3fdd9b640982317df7f44566741e7686e51029e4c4fe4cc",
7
- "tables": [
8
- {
9
- "name": "notes",
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": "body",
24
- "type": "string",
25
- "nullable": false
26
- },
27
- {
28
- "name": "updated_at_ms",
29
- "type": "integer",
30
- "nullable": false
31
- }
32
- ]
33
- }
34
- ]
6
+ "sha256": "87b23cd0f8e3677ca6fa7ca67eb7c8373a285607b6de10854cc43df1290bb0fa"
35
7
  }
36
- ]
8
+ ],
9
+ "head": {
10
+ "tables": [
11
+ {
12
+ "name": "todos",
13
+ "primaryKey": "id",
14
+ "columns": [
15
+ {
16
+ "name": "id",
17
+ "type": "string",
18
+ "nullable": false
19
+ },
20
+ {
21
+ "name": "list_id",
22
+ "type": "string",
23
+ "nullable": false
24
+ },
25
+ {
26
+ "name": "title",
27
+ "type": "string",
28
+ "nullable": false
29
+ },
30
+ {
31
+ "name": "done",
32
+ "type": "boolean",
33
+ "nullable": false
34
+ },
35
+ {
36
+ "name": "position",
37
+ "type": "integer",
38
+ "nullable": false
39
+ },
40
+ {
41
+ "name": "updated_at_ms",
42
+ "type": "integer",
43
+ "nullable": false
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
37
49
  }
@@ -1,6 +1,6 @@
1
1
  // Generated by @syncular/typegen — DO NOT EDIT.
2
2
  // irVersion: 1
3
- // irHash: sha256:7c07edc88f5e6f4b6da196da47e08abe4c7cb304d257a429da6950024f02fa85
3
+ // irHash: sha256:bc1a58e1aae968ebc7be7981d2bcc5f01f176719afab10e6b5b02f4bf1d2ec86
4
4
 
5
5
  export type SyqlRuntimeErrorCode =
6
6
  | 'SYQL_RUNTIME_MISSING_REQUIRED_INPUT'
@@ -65,6 +65,11 @@ export interface QueryClient {
65
65
  * `@syncular/react`'s `useQuery`. `Row` is the projection row
66
66
  * type; `Params` is `undefined` for a param-less query. `sqlFor`
67
67
  * selects a checked revision-1 SYQL physical statement when needed. */
68
+ export interface QueryRelationPlan {
69
+ readonly sql: string;
70
+ readonly relations: readonly { readonly table: string; readonly start: number; readonly end: number; readonly alias?: string }[];
71
+ }
72
+
68
73
  export interface NamedQuery<Row, Params = undefined> {
69
74
  readonly id: string;
70
75
  readonly hasParams: boolean;
@@ -72,6 +77,7 @@ export interface NamedQuery<Row, Params = undefined> {
72
77
  readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;
73
78
  readonly tables: readonly string[];
74
79
  readonly resultColumns: readonly QueryResultColumn[];
80
+ readonly relationPlans: readonly QueryRelationPlan[];
75
81
  readonly bind: (params: Params) => readonly QueryValue[];
76
82
  readonly sqlFor?: (params: Params) => string;
77
83
  readonly dependencies: (params: Params) => readonly QueryDependency[];
@@ -139,8 +145,8 @@ function listTodosValidate(raw?: ListTodosParams): ListTodosParams {
139
145
  /** Tables 'listTodos' reads (compatibility/export surface). */
140
146
  export const listTodosTables = ['todos'] as const;
141
147
 
142
- const listTodosStatements: { sql: string; bind: (params: ListTodosParams) => QueryValue[] }[] = [
143
- { sql: 'select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where todos.list_id = ? order by position, id', bind: (params) => [params.listId] },
148
+ const listTodosStatements: (QueryRelationPlan & { bind: (params: ListTodosParams) => QueryValue[] })[] = [
149
+ { sql: 'select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where todos.list_id = ? order by position, id', relations: [{"table":"todos","start":87,"end":92}], bind: (params) => [params.listId] },
144
150
  ];
145
151
  function listTodosSelect(raw: ListTodosParams): { sql: string; bind: QueryValue[] } {
146
152
  const params = listTodosValidate(raw);
@@ -159,12 +165,13 @@ export async function listTodos(client: QueryClient, params: ListTodosParams): P
159
165
 
160
166
  /** Revisioned reactive descriptor for `useQuery(listTodosQuery, params)`. */
161
167
  export const listTodosQuery: NamedQuery<ListTodosRow, ListTodosParams> = {
162
- id: 'sha256:7c07edc88f5e6f4b6da196da47e08abe4c7cb304d257a429da6950024f02fa85/listTodos',
168
+ id: 'sha256:bc1a58e1aae968ebc7be7981d2bcc5f01f176719afab10e6b5b02f4bf1d2ec86/listTodos',
163
169
  hasParams: true,
164
170
  sql: 'select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where todos.list_id = ? order by position, id',
165
171
  mapRow: listTodosMapRow,
166
172
  sqlFor: (params: ListTodosParams) => listTodosSelect(params).sql,
167
173
  tables: listTodosTables,
174
+ relationPlans: listTodosStatements,
168
175
  resultColumns: [{ name: 'id', type: 'string', nullable: false }, { name: 'listId', type: 'string', nullable: false }, { name: 'title', type: 'string', nullable: false }, { name: 'done', type: 'boolean', nullable: false }, { name: 'position', type: 'integer', nullable: false }, { name: 'updatedAtMs', type: 'integer', nullable: false }],
169
176
  dependencies: (params) => [
170
177
  { table: 'todos', scopeKeys: ['list:' + String(params.listId) + ''] },
@@ -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.15.47", features = ["native-transport"] }
24
+ tauri-plugin-syncular = { version = "0.16.1", features = ["native-transport"] }
25
25
 
26
26
  [features]
27
27
  default = []
@@ -1,5 +1,5 @@
1
1
  {
2
- "queryIrVersion": 3,
2
+ "queryIrVersion": 4,
3
3
  "queries": [
4
4
  {
5
5
  "name": "listTodos",
@@ -7,6 +7,13 @@
7
7
  "sourceSql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos\n where todos.list_id = :listId\n order by position, id",
8
8
  "sql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos\n where todos.list_id = :listId\n order by position, id",
9
9
  "positionalSql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where todos.list_id = ? order by position, id",
10
+ "relations": [
11
+ {
12
+ "table": "todos",
13
+ "start": 87,
14
+ "end": 92
15
+ }
16
+ ],
10
17
  "params": [
11
18
  {
12
19
  "name": "listId",
@@ -126,6 +133,13 @@
126
133
  "activationMask": 0,
127
134
  "sql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos\n where todos.list_id = :listId\n order by position, id",
128
135
  "positionalSql": "select id, list_id AS listId, title, done, position, updated_at_ms AS updatedAtMs from todos where todos.list_id = ? order by position, id",
136
+ "relations": [
137
+ {
138
+ "table": "todos",
139
+ "start": 87,
140
+ "end": 92
141
+ }
142
+ ],
129
143
  "binds": [
130
144
  {
131
145
  "kind": "value",