create-syncular-app 0.15.47 → 0.15.48
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 +1 -1
- package/package.json +1 -1
- package/template/minimal/migrations/0001_initial/up.sql +4 -2
- package/template/minimal/src/clients.ts +15 -14
- package/template/minimal/src/smoke.test.ts +13 -7
- package/template/minimal/src/syncular.generated.ts +27 -19
- package/template/minimal/syncular.ir.json +14 -4
- package/template/minimal/syncular.json +3 -3
- package/template/minimal/syncular.migrations.lock.json +43 -31
- package/template/tauri/src-tauri/Cargo.toml +1 -1
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 [
|
|
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,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
|
|
4
|
+
CREATE TABLE todos (
|
|
5
5
|
id TEXT PRIMARY KEY,
|
|
6
6
|
list_id TEXT NOT NULL,
|
|
7
|
-
|
|
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
|
|
5
|
-
* database
|
|
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 = '
|
|
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: '
|
|
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
|
|
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: '
|
|
29
|
+
table: 'todos',
|
|
31
30
|
op: 'upsert',
|
|
32
31
|
values: {
|
|
33
|
-
id: '
|
|
32
|
+
id: 'todo-1',
|
|
34
33
|
list_id: LIST_ID,
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
45
|
+
await b.syncUntilIdle(); // B bootstraps the list and applies A's todo
|
|
45
46
|
|
|
46
|
-
const rows = b.query('SELECT 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]?.
|
|
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 = {
|
|
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: '
|
|
56
|
+
table: 'todos',
|
|
53
57
|
op: 'upsert',
|
|
54
58
|
values: {
|
|
55
|
-
id: '
|
|
56
|
-
list_id: '
|
|
57
|
-
|
|
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,
|
|
66
|
-
expect(rows).toEqual([{ id: '
|
|
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:
|
|
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: '
|
|
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: '
|
|
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
|
|
40
|
-
export interface
|
|
41
|
+
/** One todos row (§2.4 column order). */
|
|
42
|
+
export interface TodosRow {
|
|
41
43
|
id: string;
|
|
42
44
|
listId: string;
|
|
43
|
-
|
|
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
|
|
52
|
+
export interface TodosInsert {
|
|
49
53
|
id: string;
|
|
50
54
|
listId: string;
|
|
51
|
-
|
|
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
|
|
62
|
+
export interface TodosUpdate {
|
|
57
63
|
id: string;
|
|
58
64
|
listId?: string;
|
|
59
|
-
|
|
65
|
+
title?: string;
|
|
66
|
+
done?: boolean;
|
|
67
|
+
position?: number;
|
|
60
68
|
updatedAtMs?: number;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
/** Typed mutation/resource descriptor for '
|
|
64
|
-
export const
|
|
65
|
-
name: '
|
|
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
|
|
78
|
+
export interface TodosInListParams {
|
|
71
79
|
listId: string;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
/** Requested-scope template for the '
|
|
75
|
-
export const
|
|
76
|
-
name: '
|
|
77
|
-
table: '
|
|
78
|
-
scopes: (params:
|
|
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": "
|
|
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": "
|
|
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": "
|
|
51
|
-
"table": "
|
|
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": "
|
|
9
|
+
"tables": [{ "name": "todos", "scopes": ["list:{list_id}"] }],
|
|
10
10
|
"subscriptions": [
|
|
11
11
|
{
|
|
12
|
-
"name": "
|
|
13
|
-
"table": "
|
|
12
|
+
"name": "todosInList",
|
|
13
|
+
"table": "todos",
|
|
14
14
|
"scopes": { "list_id": ["{listId}"] }
|
|
15
15
|
}
|
|
16
16
|
]
|
|
@@ -1,37 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
"formatVersion":
|
|
2
|
+
"formatVersion": 2,
|
|
3
3
|
"migrations": [
|
|
4
4
|
{
|
|
5
5
|
"name": "0001_initial",
|
|
6
|
-
"sha256": "
|
|
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
|
}
|
|
@@ -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.
|
|
24
|
+
tauri-plugin-syncular = { version = "0.15.48", features = ["native-transport"] }
|
|
25
25
|
|
|
26
26
|
[features]
|
|
27
27
|
default = []
|