@syncular/testkit 0.1.3 → 0.2.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.
Files changed (94) hide show
  1. package/README.md +210 -0
  2. package/dist/client.d.ts +68 -0
  3. package/dist/client.js +145 -0
  4. package/dist/clock.d.ts +24 -0
  5. package/dist/clock.js +32 -0
  6. package/dist/create-test-sync.d.ts +67 -0
  7. package/dist/create-test-sync.js +90 -0
  8. package/dist/faults.d.ts +52 -50
  9. package/dist/faults.js +74 -192
  10. package/dist/index.d.ts +17 -11
  11. package/dist/index.js +18 -11
  12. package/dist/react.d.ts +10 -0
  13. package/dist/react.js +41 -0
  14. package/dist/server.d.ts +44 -4
  15. package/dist/server.js +68 -4
  16. package/package.json +41 -57
  17. package/src/client.ts +218 -0
  18. package/src/clock.ts +45 -0
  19. package/src/create-test-sync.ts +158 -0
  20. package/src/faults.ts +77 -338
  21. package/src/index.ts +44 -10
  22. package/src/react.ts +46 -0
  23. package/src/server.ts +102 -3
  24. package/dist/audit.d.ts +0 -22
  25. package/dist/audit.d.ts.map +0 -1
  26. package/dist/audit.js +0 -40
  27. package/dist/audit.js.map +0 -1
  28. package/dist/client-bridge.d.ts +0 -77
  29. package/dist/client-bridge.d.ts.map +0 -1
  30. package/dist/client-bridge.js +0 -540
  31. package/dist/client-bridge.js.map +0 -1
  32. package/dist/deterministic.d.ts +0 -31
  33. package/dist/deterministic.d.ts.map +0 -1
  34. package/dist/deterministic.js +0 -69
  35. package/dist/deterministic.js.map +0 -1
  36. package/dist/disposable.d.ts +0 -10
  37. package/dist/disposable.d.ts.map +0 -1
  38. package/dist/disposable.js +0 -28
  39. package/dist/disposable.js.map +0 -1
  40. package/dist/faults.d.ts.map +0 -1
  41. package/dist/faults.js.map +0 -1
  42. package/dist/hono-node-server.d.ts +0 -20
  43. package/dist/hono-node-server.d.ts.map +0 -1
  44. package/dist/hono-node-server.js +0 -90
  45. package/dist/hono-node-server.js.map +0 -1
  46. package/dist/http-fixtures.d.ts +0 -32
  47. package/dist/http-fixtures.d.ts.map +0 -1
  48. package/dist/http-fixtures.js +0 -117
  49. package/dist/http-fixtures.js.map +0 -1
  50. package/dist/index.d.ts.map +0 -1
  51. package/dist/index.js.map +0 -1
  52. package/dist/project-scoped-tasks.d.ts +0 -40
  53. package/dist/project-scoped-tasks.d.ts.map +0 -1
  54. package/dist/project-scoped-tasks.js +0 -247
  55. package/dist/project-scoped-tasks.js.map +0 -1
  56. package/dist/realtime-ws.d.ts +0 -48
  57. package/dist/realtime-ws.d.ts.map +0 -1
  58. package/dist/realtime-ws.js +0 -203
  59. package/dist/realtime-ws.js.map +0 -1
  60. package/dist/runtime-process.d.ts +0 -14
  61. package/dist/runtime-process.d.ts.map +0 -1
  62. package/dist/runtime-process.js +0 -99
  63. package/dist/runtime-process.js.map +0 -1
  64. package/dist/server.d.ts.map +0 -1
  65. package/dist/server.js.map +0 -1
  66. package/dist/sync-builders.d.ts +0 -48
  67. package/dist/sync-builders.d.ts.map +0 -1
  68. package/dist/sync-builders.js +0 -66
  69. package/dist/sync-builders.js.map +0 -1
  70. package/dist/sync-http.d.ts +0 -48
  71. package/dist/sync-http.d.ts.map +0 -1
  72. package/dist/sync-http.js +0 -43
  73. package/dist/sync-http.js.map +0 -1
  74. package/dist/sync-parse.d.ts +0 -8
  75. package/dist/sync-parse.d.ts.map +0 -1
  76. package/dist/sync-parse.js +0 -38
  77. package/dist/sync-parse.js.map +0 -1
  78. package/dist/sync-response.d.ts +0 -6
  79. package/dist/sync-response.d.ts.map +0 -1
  80. package/dist/sync-response.js +0 -19
  81. package/dist/sync-response.js.map +0 -1
  82. package/src/audit.ts +0 -74
  83. package/src/client-bridge.ts +0 -841
  84. package/src/deterministic.ts +0 -121
  85. package/src/disposable.ts +0 -50
  86. package/src/hono-node-server.ts +0 -144
  87. package/src/http-fixtures.ts +0 -187
  88. package/src/project-scoped-tasks.ts +0 -351
  89. package/src/realtime-ws.ts +0 -354
  90. package/src/runtime-process.ts +0 -159
  91. package/src/sync-builders.ts +0 -136
  92. package/src/sync-http.ts +0 -117
  93. package/src/sync-parse.ts +0 -86
  94. package/src/sync-response.ts +0 -43
@@ -1,121 +0,0 @@
1
- export interface CreateIdFactoryOptions {
2
- prefix?: string;
3
- separator?: string;
4
- startAt?: number;
5
- step?: number;
6
- padLength?: number;
7
- }
8
-
9
- export interface IdFactory {
10
- next: () => string;
11
- peek: () => string;
12
- current: () => number;
13
- reset: (startAt?: number) => void;
14
- }
15
-
16
- function normalizeCounter(value: number, label: string): number {
17
- if (!Number.isFinite(value)) {
18
- throw new Error(`${label} must be a finite number`);
19
- }
20
-
21
- return Math.trunc(value);
22
- }
23
-
24
- function formatCounter(value: number, padLength: number): string {
25
- const text = String(value);
26
- return padLength > text.length ? text.padStart(padLength, '0') : text;
27
- }
28
-
29
- export function createIdFactory(
30
- options: CreateIdFactoryOptions = {}
31
- ): IdFactory {
32
- const prefix = options.prefix ?? '';
33
- const separator = options.separator ?? '-';
34
- const step = normalizeCounter(options.step ?? 1, 'step');
35
- const padLength = Math.max(
36
- 0,
37
- normalizeCounter(options.padLength ?? 0, 'padLength')
38
- );
39
-
40
- if (step === 0) {
41
- throw new Error('step must not be 0');
42
- }
43
-
44
- let counter = normalizeCounter(options.startAt ?? 1, 'startAt');
45
-
46
- const format = (value: number): string => {
47
- const token = formatCounter(value, padLength);
48
- if (prefix.length === 0) {
49
- return token;
50
- }
51
-
52
- return `${prefix}${separator}${token}`;
53
- };
54
-
55
- return {
56
- next: () => {
57
- const value = format(counter);
58
- counter += step;
59
- return value;
60
- },
61
- peek: () => format(counter),
62
- current: () => counter,
63
- reset: (nextStartAt = options.startAt ?? 1) => {
64
- counter = normalizeCounter(nextStartAt, 'startAt');
65
- },
66
- };
67
- }
68
-
69
- export interface CreateCommitIdFactoryOptions
70
- extends Omit<CreateIdFactoryOptions, 'prefix'> {
71
- prefix?: string;
72
- }
73
-
74
- export function createCommitIdFactory(
75
- options: CreateCommitIdFactoryOptions = {}
76
- ): IdFactory {
77
- return createIdFactory({
78
- prefix: options.prefix ?? 'commit',
79
- separator: options.separator,
80
- startAt: options.startAt,
81
- step: options.step,
82
- padLength: options.padLength,
83
- });
84
- }
85
-
86
- export interface CreateFakeClockOptions {
87
- startMs?: number;
88
- tickMs?: number;
89
- }
90
-
91
- export interface FakeClock {
92
- now: () => number;
93
- iso: () => string;
94
- set: (nextMs: number) => number;
95
- advance: (deltaMs: number) => number;
96
- tick: (stepMs?: number) => number;
97
- }
98
-
99
- export function createFakeClock(
100
- options: CreateFakeClockOptions = {}
101
- ): FakeClock {
102
- const defaultTickMs = normalizeCounter(options.tickMs ?? 1, 'tickMs');
103
- let nowMs = normalizeCounter(options.startMs ?? Date.now(), 'startMs');
104
-
105
- return {
106
- now: () => nowMs,
107
- iso: () => new Date(nowMs).toISOString(),
108
- set: (nextMs: number) => {
109
- nowMs = normalizeCounter(nextMs, 'nextMs');
110
- return nowMs;
111
- },
112
- advance: (deltaMs: number) => {
113
- nowMs += normalizeCounter(deltaMs, 'deltaMs');
114
- return nowMs;
115
- },
116
- tick: (stepMs = defaultTickMs) => {
117
- nowMs += normalizeCounter(stepMs, 'stepMs');
118
- return nowMs;
119
- },
120
- };
121
- }
package/src/disposable.ts DELETED
@@ -1,50 +0,0 @@
1
- export interface AsyncDisposableResource<T> {
2
- value: T;
3
- dispose: () => Promise<void>;
4
- [Symbol.asyncDispose]: () => Promise<void>;
5
- }
6
-
7
- export type ResourceRunner<T, TResult> =
8
- | ((value: T) => Promise<TResult>)
9
- | ((value: T) => TResult);
10
-
11
- export function createAsyncDisposableResource<T>(
12
- value: T,
13
- dispose: () => Promise<void> | void
14
- ): AsyncDisposableResource<T> {
15
- let disposed = false;
16
-
17
- const disposeOnce = async () => {
18
- if (disposed) {
19
- return;
20
- }
21
-
22
- disposed = true;
23
- await dispose();
24
- };
25
-
26
- return {
27
- value,
28
- dispose: disposeOnce,
29
- [Symbol.asyncDispose]: disposeOnce,
30
- };
31
- }
32
-
33
- export async function withAsyncDisposableResource<T, TResult>(
34
- resource: AsyncDisposableResource<T>,
35
- run: ResourceRunner<T, TResult>
36
- ): Promise<TResult> {
37
- try {
38
- return await run(resource.value);
39
- } finally {
40
- await resource.dispose();
41
- }
42
- }
43
-
44
- export async function withAsyncDisposableFactory<T, TResult>(
45
- create: () => Promise<AsyncDisposableResource<T>>,
46
- run: ResourceRunner<T, TResult>
47
- ): Promise<TResult> {
48
- const resource = await create();
49
- return withAsyncDisposableResource(resource, run);
50
- }
@@ -1,144 +0,0 @@
1
- import { createServer, type Server as NodeServer } from 'node:http';
2
- import type { Hono } from 'hono';
3
- import {
4
- type AsyncDisposableResource,
5
- createAsyncDisposableResource,
6
- type ResourceRunner,
7
- withAsyncDisposableFactory,
8
- } from './disposable';
9
-
10
- export interface NodeHonoServerOptions {
11
- cors?: boolean;
12
- corsAllowMethods?: string;
13
- corsAllowHeaders?: string;
14
- corsMaxAgeSeconds?: number;
15
- }
16
-
17
- export interface CreateNodeHonoServerResourceOptions
18
- extends NodeHonoServerOptions {
19
- listen?: boolean;
20
- host?: string;
21
- port?: number;
22
- }
23
-
24
- export function createNodeHonoServer(
25
- app: Hono,
26
- options?: NodeHonoServerOptions
27
- ): NodeServer {
28
- const corsEnabled = options?.cors ?? true;
29
- const corsAllowMethods =
30
- options?.corsAllowMethods ?? 'GET, POST, PUT, DELETE, OPTIONS';
31
- const corsAllowHeaders =
32
- options?.corsAllowHeaders ??
33
- 'authorization, content-type, x-actor-id, x-partition-id, x-syncular-schema-version, x-syncular-snapshot-scopes, x-syncular-transport-path, x-upload-token, x-user-id';
34
- const corsMaxAgeSeconds = options?.corsMaxAgeSeconds ?? 86400;
35
-
36
- return createServer(async (req, res) => {
37
- const url = `http://localhost${req.url ?? '/'}`;
38
-
39
- const headers = new Headers();
40
- for (const [key, value] of Object.entries(req.headers)) {
41
- if (!value) {
42
- continue;
43
- }
44
-
45
- headers.set(key, Array.isArray(value) ? value.join(', ') : value);
46
- }
47
-
48
- if (corsEnabled && req.method === 'OPTIONS') {
49
- res.writeHead(204, {
50
- 'access-control-allow-origin': '*',
51
- 'access-control-allow-methods': corsAllowMethods,
52
- 'access-control-allow-headers': corsAllowHeaders,
53
- 'access-control-max-age': String(corsMaxAgeSeconds),
54
- });
55
- res.end();
56
- return;
57
- }
58
-
59
- const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
60
- const body = hasBody
61
- ? await new Promise<Uint8Array>((resolve) => {
62
- const chunks: Uint8Array[] = [];
63
- req.on('data', (chunk: Uint8Array) => chunks.push(chunk));
64
- req.on('end', () => {
65
- const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
66
- const merged = new Uint8Array(total);
67
- let offset = 0;
68
- for (const chunk of chunks) {
69
- merged.set(chunk, offset);
70
- offset += chunk.length;
71
- }
72
- resolve(merged);
73
- });
74
- })
75
- : undefined;
76
-
77
- const requestBody = body ? Uint8Array.from(body) : undefined;
78
-
79
- const request = new Request(url, {
80
- method: req.method,
81
- headers,
82
- body: requestBody,
83
- });
84
-
85
- const response = await app.fetch(request);
86
- const responseHeaders: Record<string, string> = {};
87
- response.headers.forEach((value, key) => {
88
- responseHeaders[key] = value;
89
- });
90
-
91
- if (corsEnabled) {
92
- responseHeaders['access-control-allow-origin'] = '*';
93
- }
94
-
95
- res.writeHead(response.status, responseHeaders);
96
- const bytes = Buffer.from(await response.arrayBuffer());
97
- res.end(bytes);
98
- });
99
- }
100
-
101
- export async function closeNodeServer(server: NodeServer): Promise<void> {
102
- await new Promise<void>((resolve, reject) => {
103
- server.close((error) => {
104
- if (error) {
105
- reject(error);
106
- return;
107
- }
108
-
109
- resolve();
110
- });
111
- });
112
- }
113
-
114
- export function createNodeServerResource(
115
- server: NodeServer
116
- ): AsyncDisposableResource<NodeServer> {
117
- return createAsyncDisposableResource(server, () => closeNodeServer(server));
118
- }
119
-
120
- export async function createNodeHonoServerResource(
121
- app: Hono,
122
- options: CreateNodeHonoServerResourceOptions = {}
123
- ): Promise<AsyncDisposableResource<NodeServer>> {
124
- const server = createNodeHonoServer(app, options);
125
-
126
- if (options.listen ?? true) {
127
- await new Promise<void>((resolve) => {
128
- server.listen(options.port ?? 0, options.host ?? '127.0.0.1', resolve);
129
- });
130
- }
131
-
132
- return createNodeServerResource(server);
133
- }
134
-
135
- export async function withNodeHonoServer<TResult>(
136
- app: Hono,
137
- run: ResourceRunner<NodeServer, TResult>,
138
- options: CreateNodeHonoServerResourceOptions = {}
139
- ): Promise<TResult> {
140
- return withAsyncDisposableFactory(
141
- () => createNodeHonoServerResource(app, options),
142
- run
143
- );
144
- }
@@ -1,187 +0,0 @@
1
- import { createDatabase } from '@syncular/core';
2
- import {
3
- ensureSyncSchema,
4
- type ServerSyncDialect,
5
- type ServerTableHandler,
6
- type SyncCoreDb,
7
- } from '@syncular/server';
8
- import { createBunSqliteDialect } from '@syncular/server/bun-sqlite';
9
- import {
10
- type CreateSyncRoutesOptions,
11
- createSyncRoutes,
12
- } from '@syncular/server/hono';
13
- import { createPgliteDialect } from '@syncular/server/pglite';
14
- import { createPostgresServerDialect } from '@syncular/server/postgres';
15
- import { createSqliteServerDialect } from '@syncular/server/sqlite';
16
- import { Hono } from 'hono';
17
- import type { Kysely } from 'kysely';
18
- import { createNodeHonoServer } from './hono-node-server';
19
-
20
- export type HttpServerDialect = 'sqlite' | 'pglite';
21
-
22
- export interface HttpServerFixture<DB extends SyncCoreDb> {
23
- db: Kysely<DB>;
24
- dialect: ServerSyncDialect;
25
- app: Hono;
26
- httpServer: ReturnType<typeof createNodeHonoServer>;
27
- baseUrl: string;
28
- destroy: () => Promise<void>;
29
- }
30
-
31
- export interface CreateHttpServerFixtureOptions<DB extends SyncCoreDb> {
32
- serverDialect: HttpServerDialect;
33
- createTables: (db: Kysely<DB>) => Promise<void>;
34
- handlers: ServerTableHandler<DB>[];
35
- authenticate: CreateSyncRoutesOptions<
36
- DB,
37
- { actorId: string }
38
- >['authenticate'];
39
- sync?: CreateSyncRoutesOptions<DB, { actorId: string }>['sync'];
40
- routePath?: string;
41
- cors?: boolean;
42
- corsAllowMethods?: string;
43
- corsAllowHeaders?: string;
44
- corsMaxAgeSeconds?: number;
45
- }
46
-
47
- const PGLITE_INIT_ATTEMPTS = 3;
48
-
49
- function isTransientPgliteInitError(error: Error): boolean {
50
- return (
51
- error.message.includes('access to a null reference') ||
52
- error.message.includes('_pgl_initdb')
53
- );
54
- }
55
-
56
- function isServerNotRunningError(error: Error | null): boolean {
57
- return (
58
- error !== null && 'code' in error && error.code === 'ERR_SERVER_NOT_RUNNING'
59
- );
60
- }
61
-
62
- async function delay(ms: number): Promise<void> {
63
- await new Promise<void>((resolve) => {
64
- setTimeout(resolve, ms);
65
- });
66
- }
67
-
68
- async function withPgliteInitRetry<T>(
69
- useRetries: boolean,
70
- create: () => Promise<T>
71
- ): Promise<T> {
72
- const maxAttempts = useRetries ? PGLITE_INIT_ATTEMPTS : 1;
73
- let lastError: Error | null = null;
74
-
75
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
76
- try {
77
- return await create();
78
- } catch (error) {
79
- const candidate =
80
- error instanceof Error ? error : new Error(String(error));
81
- lastError = candidate;
82
-
83
- const isRetryable =
84
- useRetries &&
85
- attempt < maxAttempts &&
86
- isTransientPgliteInitError(candidate);
87
- if (!isRetryable) {
88
- throw candidate;
89
- }
90
-
91
- await delay(25 * attempt);
92
- }
93
- }
94
-
95
- throw lastError ?? new Error('PGLite initialization failed');
96
- }
97
-
98
- export async function createHttpServerFixture<DB extends SyncCoreDb>(
99
- options: CreateHttpServerFixtureOptions<DB>
100
- ): Promise<HttpServerFixture<DB>> {
101
- const dialect =
102
- options.serverDialect === 'pglite'
103
- ? createPostgresServerDialect()
104
- : createSqliteServerDialect();
105
-
106
- const db = await withPgliteInitRetry(
107
- options.serverDialect === 'pglite',
108
- async () => {
109
- const candidateDb =
110
- options.serverDialect === 'pglite'
111
- ? createDatabase<DB>({
112
- dialect: createPgliteDialect(),
113
- family: 'postgres',
114
- })
115
- : createDatabase<DB>({
116
- dialect: createBunSqliteDialect({ path: ':memory:' }),
117
- family: 'sqlite',
118
- });
119
-
120
- try {
121
- await ensureSyncSchema(candidateDb, dialect);
122
- if (dialect.ensureConsoleSchema) {
123
- await dialect.ensureConsoleSchema(candidateDb);
124
- }
125
- await options.createTables(candidateDb);
126
- return candidateDb;
127
- } catch (error) {
128
- await candidateDb.destroy();
129
- throw error;
130
- }
131
- }
132
- );
133
-
134
- const app = new Hono();
135
- const routePath = options.routePath ?? '/sync';
136
-
137
- const syncRoutes = createSyncRoutes<DB>({
138
- db,
139
- dialect,
140
- handlers: options.handlers,
141
- authenticate: options.authenticate,
142
- sync: options.sync,
143
- });
144
-
145
- app.route(routePath, syncRoutes);
146
-
147
- const httpServer = createNodeHonoServer(app, {
148
- cors: options.cors,
149
- corsAllowMethods: options.corsAllowMethods,
150
- corsAllowHeaders: options.corsAllowHeaders,
151
- corsMaxAgeSeconds: options.corsMaxAgeSeconds,
152
- });
153
-
154
- await new Promise<void>((resolve) => {
155
- httpServer.listen(0, resolve);
156
- });
157
-
158
- const address = httpServer.address();
159
- const port = typeof address === 'object' && address ? address.port : 0;
160
-
161
- let destroyed = false;
162
-
163
- return {
164
- db,
165
- dialect,
166
- app,
167
- httpServer,
168
- baseUrl: `http://localhost:${port}`,
169
- destroy: async () => {
170
- if (destroyed) {
171
- return;
172
- }
173
- destroyed = true;
174
-
175
- await new Promise<void>((resolve, reject) => {
176
- httpServer.close((err) => {
177
- if (err && !isServerNotRunningError(err)) {
178
- reject(err);
179
- return;
180
- }
181
- resolve();
182
- });
183
- });
184
- await db.destroy();
185
- },
186
- };
187
- }