@syncular/testkit 0.8.0 → 0.9.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,35 @@ 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
+
171
196
  ## React
172
197
 
173
198
  `@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 { 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,8 @@ 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;
40
42
  /** Epoch ms the shared virtual clock starts at (default 1_750_000_000_000). */
41
43
  readonly startMs?: number;
42
44
  }
@@ -28,6 +28,9 @@ 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
+ : {}),
31
34
  });
32
35
  const clients = [];
33
36
  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 { 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,8 @@ 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;
30
32
  }
31
33
  /**
32
34
  * The server half of a test sync. `ctxFor(actorId)` builds the per-request
package/dist/server.js CHANGED
@@ -45,6 +45,9 @@ export function createTestServer(options) {
45
45
  resolveScopes,
46
46
  clock: clockFn,
47
47
  segments,
48
+ ...(options.validators !== undefined
49
+ ? { validators: options.validators }
50
+ : {}),
48
51
  });
49
52
  return {
50
53
  storage,
@@ -58,6 +61,9 @@ export function createTestServer(options) {
58
61
  storage,
59
62
  segments,
60
63
  resolveScopes,
64
+ ...(options.validators !== undefined
65
+ ? { validators: options.validators }
66
+ : {}),
61
67
  clock: clockFn,
62
68
  realtime: hub,
63
69
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/testkit",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.9.0",
68
+ "@syncular/client": "0.9.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.9.0",
81
+ "@syncular/react": "0.9.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,7 @@
16
16
  */
17
17
 
18
18
  import type { ClientSchema, SyncClientConfig } from '@syncular/client';
19
- import type { ResolveScopes } from '@syncular/server';
19
+ import type { ResolveScopes, ValidatorRegistry } from '@syncular/server';
20
20
  import { buildTestClient, type TestClient } from './client';
21
21
  import { createVirtualClock, type VirtualClock } from './clock';
22
22
  import {
@@ -44,6 +44,8 @@ export interface CreateTestSyncOptions {
44
44
  * in the server exactly as it would in production.
45
45
  */
46
46
  readonly resolveScopes?: ResolveScopes;
47
+ /** Optional §6.7 write validators, executed by the real test server. */
48
+ readonly validators?: ValidatorRegistry;
47
49
  /** Epoch ms the shared virtual clock starts at (default 1_750_000_000_000). */
48
50
  readonly startMs?: number;
49
51
  }
@@ -98,6 +100,9 @@ export async function createTestSync(
98
100
  ...(options.resolveScopes !== undefined
99
101
  ? { resolveScopes: options.resolveScopes }
100
102
  : {}),
103
+ ...(options.validators !== undefined
104
+ ? { validators: options.validators }
105
+ : {}),
101
106
  });
102
107
 
103
108
  const clients: TestClient[] = [];
package/src/server.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  type ServerSchema,
18
18
  SqliteServerStorage,
19
19
  type SyncRequestContext,
20
+ type ValidatorRegistry,
20
21
  } from '@syncular/server';
21
22
  import type { VirtualClock } from './clock';
22
23
 
@@ -50,6 +51,8 @@ export interface TestServerOptions {
50
51
  readonly partition: string;
51
52
  /** Defaults to {@link allowAllScopes}. */
52
53
  readonly resolveScopes?: ResolveScopes;
54
+ /** Optional §6.7 validators, shared by HTTP-like and socket rounds. */
55
+ readonly validators?: ValidatorRegistry;
53
56
  }
54
57
 
55
58
  /**
@@ -79,6 +82,9 @@ export function createTestServer(options: TestServerOptions): TestServer {
79
82
  resolveScopes,
80
83
  clock: clockFn,
81
84
  segments,
85
+ ...(options.validators !== undefined
86
+ ? { validators: options.validators }
87
+ : {}),
82
88
  });
83
89
  return {
84
90
  storage,
@@ -92,6 +98,9 @@ export function createTestServer(options: TestServerOptions): TestServer {
92
98
  storage,
93
99
  segments,
94
100
  resolveScopes,
101
+ ...(options.validators !== undefined
102
+ ? { validators: options.validators }
103
+ : {}),
95
104
  clock: clockFn,
96
105
  realtime: hub,
97
106
  }),