@syncular/testkit 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,15 +16,10 @@ green test here is real sync behaviour, not a fake.
16
16
 
17
17
  ## Install
18
18
 
19
- Workspace-internal only:
20
-
21
- ```jsonc
22
- // package.json
23
- {
24
- "devDependencies": {
25
- "@syncular/testkit": "workspace:*"
26
- }
27
- }
19
+ Install it as a development dependency:
20
+
21
+ ```bash
22
+ bun add --dev @syncular/testkit
28
23
  ```
29
24
 
30
25
  Requires the Bun runtime (the in-memory client backend is `bun:sqlite`). The
@@ -71,6 +66,7 @@ adds the test-only controls below.
71
66
  | `partition` | `"test"` | the §1.1 partition every client lives in |
72
67
  | `actorId` | `"test-actor"` | default actor a client authenticates as |
73
68
  | `resolveScopes` | grant-all | host authorization (§3.2); omit to grant `'*'` for every var |
69
+ | `validators` | off | real per-table write validators (§6.7), including structured rejection details |
74
70
  | `startMs` | `1_750_000_000_000` | epoch ms the shared virtual clock starts at |
75
71
 
76
72
  `TestSync`:
@@ -168,6 +164,41 @@ await a.sync(); // the hub fans the commit to b as a delta
168
164
 
169
165
  `goOffline()` also drops the socket; reconnect with `connectRealtime()`.
170
166
 
167
+ ### Multi-client conflicts and correction metadata
168
+
169
+ Create the common server row, let both clients pull it, then make their edits
170
+ from the same observed version. Push one client first so the second write is a
171
+ deterministic conflict—no timers or transport mocks are needed:
172
+
173
+ ```ts
174
+ await sync.syncAll();
175
+ const [{ version }] = a.api.query(
176
+ 'SELECT _sync_version AS version FROM notes WHERE id = ?',
177
+ ['n1'],
178
+ ) as Array<{ version: number }>;
179
+
180
+ a.api.patch('notes', 'n1', { body: 'A' }, { baseVersion: version });
181
+ b.api.patch('notes', 'n1', { body: 'B' }, { baseVersion: version });
182
+
183
+ await a.sync(); // wins version + 1
184
+ await b.sync(); // loses against the stale base
185
+
186
+ expect(b.api.conflicts[0]?.serverRow.body).toBe('A');
187
+ expect(b.api.conflicts[0]?.operation?.changedFields).toEqual(['body']);
188
+ ```
189
+
190
+ Pass `validators` to `createTestSync` to exercise the same business rules as
191
+ production. A `ValidationRejection` with structured details reaches
192
+ `client.api.rejections` through both loopback and socket sync rounds, so app
193
+ tests can assert field focus and correction routing without displaying server
194
+ diagnostics.
195
+
196
+ Pass `commitValidator` to test aggregate invariants with the same
197
+ transaction-scoped candidate view as production. Throw
198
+ `CommitValidationRejection` to select the operation that correction UI should
199
+ focus. The test server uses real SQLite transaction serialization and shares
200
+ the callback across loopback and socket rounds.
201
+
171
202
  ## React
172
203
 
173
204
  `@syncular/react`'s `SyncProvider` already takes any `SyncClient`, so
@@ -15,7 +15,7 @@
15
15
  * await sync.dispose();
16
16
  */
17
17
  import type { ClientSchema, SyncClientConfig } from '@syncular/client';
18
- import type { ResolveScopes } from '@syncular/server';
18
+ import type { CommitValidator, ResolveScopes, ValidatorRegistry } from '@syncular/server';
19
19
  import { type TestClient } from './client.js';
20
20
  import { type VirtualClock } from './clock.js';
21
21
  import { type TestServer } from './server.js';
@@ -37,6 +37,10 @@ export interface CreateTestSyncOptions {
37
37
  * in the server exactly as it would in production.
38
38
  */
39
39
  readonly resolveScopes?: ResolveScopes;
40
+ /** Optional §6.7 write validators, executed by the real test server. */
41
+ readonly validators?: ValidatorRegistry;
42
+ /** Optional §6.8 whole-commit validator, executed by the real test server. */
43
+ readonly commitValidator?: CommitValidator;
40
44
  /** Epoch ms the shared virtual clock starts at (default 1_750_000_000_000). */
41
45
  readonly startMs?: number;
42
46
  }
@@ -28,6 +28,12 @@ export async function createTestSync(options) {
28
28
  ...(options.resolveScopes !== undefined
29
29
  ? { resolveScopes: options.resolveScopes }
30
30
  : {}),
31
+ ...(options.validators !== undefined
32
+ ? { validators: options.validators }
33
+ : {}),
34
+ ...(options.commitValidator !== undefined
35
+ ? { commitValidator: options.commitValidator }
36
+ : {}),
31
37
  });
32
38
  const clients = [];
33
39
  let autoId = 0;
package/dist/server.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * directly.
9
9
  */
10
10
  import { type ClientSchema } from '@syncular/client';
11
- import { MemorySegmentStore, type RealtimeHub, type ResolveScopes, SqliteServerStorage, type SyncRequestContext } from '@syncular/server';
11
+ import { type CommitValidator, MemorySegmentStore, type RealtimeHub, type ResolveScopes, SqliteServerStorage, type SyncRequestContext, type ValidatorRegistry } from '@syncular/server';
12
12
  import type { VirtualClock } from './clock.js';
13
13
  /** Default partition every actor lives in (apps override via options). */
14
14
  export declare const DEFAULT_PARTITION = "test";
@@ -27,6 +27,10 @@ export interface TestServerOptions {
27
27
  readonly partition: string;
28
28
  /** Defaults to {@link allowAllScopes}. */
29
29
  readonly resolveScopes?: ResolveScopes;
30
+ /** Optional §6.7 validators, shared by HTTP-like and socket rounds. */
31
+ readonly validators?: ValidatorRegistry;
32
+ /** Optional §6.8 whole-commit validator, shared by both transports. */
33
+ readonly commitValidator?: CommitValidator;
30
34
  }
31
35
  /**
32
36
  * The server half of a test sync. `ctxFor(actorId)` builds the per-request
package/dist/server.js CHANGED
@@ -45,6 +45,12 @@ export function createTestServer(options) {
45
45
  resolveScopes,
46
46
  clock: clockFn,
47
47
  segments,
48
+ ...(options.validators !== undefined
49
+ ? { validators: options.validators }
50
+ : {}),
51
+ ...(options.commitValidator !== undefined
52
+ ? { commitValidator: options.commitValidator }
53
+ : {}),
48
54
  });
49
55
  return {
50
56
  storage,
@@ -58,6 +64,12 @@ export function createTestServer(options) {
58
64
  storage,
59
65
  segments,
60
66
  resolveScopes,
67
+ ...(options.validators !== undefined
68
+ ? { validators: options.validators }
69
+ : {}),
70
+ ...(options.commitValidator !== undefined
71
+ ? { commitValidator: options.commitValidator }
72
+ : {}),
61
73
  clock: clockFn,
62
74
  realtime: hub,
63
75
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/testkit",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Syncular test kit: in-process loopback server + clients for integration scenarios",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -64,8 +64,8 @@
64
64
  "test": "bun test --preload ./test/setup.ts"
65
65
  },
66
66
  "dependencies": {
67
- "@syncular/server": "0.8.0",
68
- "@syncular/client": "0.8.0"
67
+ "@syncular/server": "0.10.0",
68
+ "@syncular/client": "0.10.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": ">=18.0.0"
@@ -77,8 +77,8 @@
77
77
  },
78
78
  "devDependencies": {
79
79
  "@happy-dom/global-registrator": "^15.11.0",
80
- "@syncular/core": "0.8.0",
81
- "@syncular/react": "0.8.0",
80
+ "@syncular/core": "0.10.0",
81
+ "@syncular/react": "0.10.0",
82
82
  "@testing-library/react": "^16.1.0",
83
83
  "@types/react": "^18.3.0",
84
84
  "react": "^18.3.1",
@@ -16,7 +16,11 @@
16
16
  */
17
17
 
18
18
  import type { ClientSchema, SyncClientConfig } from '@syncular/client';
19
- import type { ResolveScopes } from '@syncular/server';
19
+ import type {
20
+ CommitValidator,
21
+ ResolveScopes,
22
+ ValidatorRegistry,
23
+ } from '@syncular/server';
20
24
  import { buildTestClient, type TestClient } from './client';
21
25
  import { createVirtualClock, type VirtualClock } from './clock';
22
26
  import {
@@ -44,6 +48,10 @@ export interface CreateTestSyncOptions {
44
48
  * in the server exactly as it would in production.
45
49
  */
46
50
  readonly resolveScopes?: ResolveScopes;
51
+ /** Optional §6.7 write validators, executed by the real test server. */
52
+ readonly validators?: ValidatorRegistry;
53
+ /** Optional §6.8 whole-commit validator, executed by the real test server. */
54
+ readonly commitValidator?: CommitValidator;
47
55
  /** Epoch ms the shared virtual clock starts at (default 1_750_000_000_000). */
48
56
  readonly startMs?: number;
49
57
  }
@@ -98,6 +106,12 @@ export async function createTestSync(
98
106
  ...(options.resolveScopes !== undefined
99
107
  ? { resolveScopes: options.resolveScopes }
100
108
  : {}),
109
+ ...(options.validators !== undefined
110
+ ? { validators: options.validators }
111
+ : {}),
112
+ ...(options.commitValidator !== undefined
113
+ ? { commitValidator: options.commitValidator }
114
+ : {}),
101
115
  });
102
116
 
103
117
  const clients: TestClient[] = [];
package/src/server.ts CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { type ClientSchema, compileClientSchema } from '@syncular/client';
12
12
  import {
13
+ type CommitValidator,
13
14
  createRealtimeHub,
14
15
  MemorySegmentStore,
15
16
  type RealtimeHub,
@@ -17,6 +18,7 @@ import {
17
18
  type ServerSchema,
18
19
  SqliteServerStorage,
19
20
  type SyncRequestContext,
21
+ type ValidatorRegistry,
20
22
  } from '@syncular/server';
21
23
  import type { VirtualClock } from './clock';
22
24
 
@@ -50,6 +52,10 @@ export interface TestServerOptions {
50
52
  readonly partition: string;
51
53
  /** Defaults to {@link allowAllScopes}. */
52
54
  readonly resolveScopes?: ResolveScopes;
55
+ /** Optional §6.7 validators, shared by HTTP-like and socket rounds. */
56
+ readonly validators?: ValidatorRegistry;
57
+ /** Optional §6.8 whole-commit validator, shared by both transports. */
58
+ readonly commitValidator?: CommitValidator;
53
59
  }
54
60
 
55
61
  /**
@@ -79,6 +85,12 @@ export function createTestServer(options: TestServerOptions): TestServer {
79
85
  resolveScopes,
80
86
  clock: clockFn,
81
87
  segments,
88
+ ...(options.validators !== undefined
89
+ ? { validators: options.validators }
90
+ : {}),
91
+ ...(options.commitValidator !== undefined
92
+ ? { commitValidator: options.commitValidator }
93
+ : {}),
82
94
  });
83
95
  return {
84
96
  storage,
@@ -92,6 +104,12 @@ export function createTestServer(options: TestServerOptions): TestServer {
92
104
  storage,
93
105
  segments,
94
106
  resolveScopes,
107
+ ...(options.validators !== undefined
108
+ ? { validators: options.validators }
109
+ : {}),
110
+ ...(options.commitValidator !== undefined
111
+ ? { commitValidator: options.commitValidator }
112
+ : {}),
95
113
  clock: clockFn,
96
114
  realtime: hub,
97
115
  }),