@syncular/testkit 0.0.0 → 0.0.2-136

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 (48) hide show
  1. package/dist/assertions.d.ts +61 -0
  2. package/dist/assertions.d.ts.map +1 -0
  3. package/dist/assertions.js +239 -0
  4. package/dist/assertions.js.map +1 -0
  5. package/dist/faults.d.ts +40 -0
  6. package/dist/faults.d.ts.map +1 -0
  7. package/dist/faults.js +136 -0
  8. package/dist/faults.js.map +1 -0
  9. package/dist/fixtures.d.ts +100 -0
  10. package/dist/fixtures.d.ts.map +1 -0
  11. package/dist/fixtures.js +541 -0
  12. package/dist/fixtures.js.map +1 -0
  13. package/dist/hono-node-server.d.ts +10 -0
  14. package/dist/hono-node-server.d.ts.map +1 -0
  15. package/dist/hono-node-server.js +63 -0
  16. package/dist/hono-node-server.js.map +1 -0
  17. package/dist/http-fixtures.d.ts +57 -0
  18. package/dist/http-fixtures.d.ts.map +1 -0
  19. package/dist/http-fixtures.js +107 -0
  20. package/dist/http-fixtures.js.map +1 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +10 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/project-scoped-tasks.d.ts +40 -0
  26. package/dist/project-scoped-tasks.d.ts.map +1 -0
  27. package/dist/project-scoped-tasks.js +245 -0
  28. package/dist/project-scoped-tasks.js.map +1 -0
  29. package/dist/runtime-process.d.ts +11 -0
  30. package/dist/runtime-process.d.ts.map +1 -0
  31. package/dist/runtime-process.js +92 -0
  32. package/dist/runtime-process.js.map +1 -0
  33. package/dist/sync-http.d.ts +48 -0
  34. package/dist/sync-http.d.ts.map +1 -0
  35. package/dist/sync-http.js +30 -0
  36. package/dist/sync-http.js.map +1 -0
  37. package/dist/sync-response.d.ts +7 -0
  38. package/dist/sync-response.d.ts.map +1 -0
  39. package/dist/sync-response.js +19 -0
  40. package/dist/sync-response.js.map +1 -0
  41. package/package.json +12 -12
  42. package/src/faults.ts +0 -3
  43. package/src/fixtures.ts +0 -3
  44. package/src/index.ts +3 -0
  45. package/src/project-scoped-tasks.ts +51 -1
  46. package/src/runtime-process.ts +133 -0
  47. package/src/sync-http.ts +100 -0
  48. package/src/sync-response.ts +45 -0
@@ -0,0 +1,48 @@
1
+ import type { SyncCombinedRequest, SyncCombinedResponse, SyncPullRequest, SyncPullResponse, SyncPushRequest, SyncPushResponse } from '@syncular/core';
2
+ export interface JsonActorHeadersOptions {
3
+ actorId: string;
4
+ actorHeader?: string;
5
+ extraHeaders?: Record<string, string>;
6
+ }
7
+ export declare function createJsonActorHeaders(options: JsonActorHeadersOptions): Record<string, string>;
8
+ export interface PostJsonWithActorOptions<TBody> {
9
+ fetch: typeof globalThis.fetch;
10
+ url: string;
11
+ actorId: string;
12
+ actorHeader?: string;
13
+ extraHeaders?: Record<string, string>;
14
+ body: TBody;
15
+ }
16
+ export interface PostJsonWithActorResult<TResponse> {
17
+ response: Response;
18
+ json: TResponse;
19
+ }
20
+ export declare function postJsonWithActor<TBody, TResponse>(options: PostJsonWithActorOptions<TBody>): Promise<PostJsonWithActorResult<TResponse>>;
21
+ export interface PostSyncCombinedRequestOptions {
22
+ fetch: typeof globalThis.fetch;
23
+ url: string;
24
+ actorId: string;
25
+ actorHeader?: string;
26
+ extraHeaders?: Record<string, string>;
27
+ body: SyncCombinedRequest;
28
+ }
29
+ export declare function postSyncCombinedRequest(options: PostSyncCombinedRequestOptions): Promise<PostJsonWithActorResult<SyncCombinedResponse>>;
30
+ export interface PostSyncPushRequestOptions {
31
+ fetch: typeof globalThis.fetch;
32
+ url: string;
33
+ actorId: string;
34
+ actorHeader?: string;
35
+ extraHeaders?: Record<string, string>;
36
+ body: SyncPushRequest;
37
+ }
38
+ export declare function postSyncPushRequest(options: PostSyncPushRequestOptions): Promise<PostJsonWithActorResult<SyncPushResponse>>;
39
+ export interface PostSyncPullRequestOptions {
40
+ fetch: typeof globalThis.fetch;
41
+ url: string;
42
+ actorId: string;
43
+ actorHeader?: string;
44
+ extraHeaders?: Record<string, string>;
45
+ body: SyncPullRequest;
46
+ }
47
+ export declare function postSyncPullRequest(options: PostSyncPullRequestOptions): Promise<PostJsonWithActorResult<SyncPullResponse>>;
48
+ //# sourceMappingURL=sync-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-http.d.ts","sourceRoot":"","sources":["../src/sync-http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EACjB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED,MAAM,WAAW,wBAAwB,CAAC,KAAK;IAC7C,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,EAAE,KAAK,CAAC;CACb;AAED,MAAM,WAAW,uBAAuB,CAAC,SAAS;IAChD,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EACtD,OAAO,EAAE,wBAAwB,CAAC,KAAK,CAAC,GACvC,OAAO,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC,CAa7C;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,EAAE,mBAAmB,CAAC;CAC3B;AAED,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,oBAAoB,CAAC,CAAC,CAExD;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,EAAE,eAAe,CAAC;CACvB;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,uBAAuB,CAAC,gBAAgB,CAAC,CAAC,CAEpD;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,EAAE,eAAe,CAAC;CACvB;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,uBAAuB,CAAC,gBAAgB,CAAC,CAAC,CAEpD"}
@@ -0,0 +1,30 @@
1
+ export function createJsonActorHeaders(options) {
2
+ return {
3
+ 'content-type': 'application/json',
4
+ [options.actorHeader ?? 'x-actor-id']: options.actorId,
5
+ ...options.extraHeaders,
6
+ };
7
+ }
8
+ export async function postJsonWithActor(options) {
9
+ const response = await options.fetch(options.url, {
10
+ method: 'POST',
11
+ headers: createJsonActorHeaders({
12
+ actorId: options.actorId,
13
+ actorHeader: options.actorHeader,
14
+ extraHeaders: options.extraHeaders,
15
+ }),
16
+ body: JSON.stringify(options.body),
17
+ });
18
+ const json = (await response.json());
19
+ return { response, json };
20
+ }
21
+ export async function postSyncCombinedRequest(options) {
22
+ return postJsonWithActor(options);
23
+ }
24
+ export async function postSyncPushRequest(options) {
25
+ return postJsonWithActor(options);
26
+ }
27
+ export async function postSyncPullRequest(options) {
28
+ return postJsonWithActor(options);
29
+ }
30
+ //# sourceMappingURL=sync-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-http.js","sourceRoot":"","sources":["../src/sync-http.ts"],"names":[],"mappings":"AAeA,MAAM,UAAU,sBAAsB,CACpC,OAAgC,EACR;IACxB,OAAO;QACL,cAAc,EAAE,kBAAkB;QAClC,CAAC,OAAO,CAAC,WAAW,IAAI,YAAY,CAAC,EAAE,OAAO,CAAC,OAAO;QACtD,GAAG,OAAO,CAAC,YAAY;KACxB,CAAC;AAAA,CACH;AAgBD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAwC,EACK;IAC7C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QAChD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,sBAAsB,CAAC;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,YAAY,EAAE,OAAO,CAAC,YAAY;SACnC,CAAC;QACF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;KACnC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAc,CAAC;IAClD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAAA,CAC3B;AAWD,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,OAAuC,EACiB;IACxD,OAAO,iBAAiB,CAA4C,OAAO,CAAC,CAAC;AAAA,CAC9E;AAWD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAmC,EACiB;IACpD,OAAO,iBAAiB,CAAoC,OAAO,CAAC,CAAC;AAAA,CACtE;AAWD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAmC,EACiB;IACpD,OAAO,iBAAiB,CAAoC,OAAO,CAAC,CAAC;AAAA,CACtE"}
@@ -0,0 +1,7 @@
1
+ import { type SyncChange, type SyncPullResponse } from '@syncular/core';
2
+ export type SyncChangeRecord = SyncChange;
3
+ export type SyncSubscriptionRecord = Pick<SyncPullResponse['subscriptions'][number], 'id' | 'commits'>;
4
+ export declare function subscriptionChanges(subscriptions: SyncSubscriptionRecord[] | undefined, subscriptionId: string): SyncChangeRecord[];
5
+ export declare function findSubscriptionChange(subscriptions: SyncSubscriptionRecord[] | undefined, subscriptionId: string, rowId: string): SyncChangeRecord | undefined;
6
+ export declare function subscriptionChangeRow(change: SyncChangeRecord | undefined): Record<string, unknown> | undefined;
7
+ //# sourceMappingURL=sync-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-response.d.ts","sourceRoot":"","sources":["../src/sync-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAE1C,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,gBAAgB,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,EACzC,IAAI,GAAG,SAAS,CACjB,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,sBAAsB,EAAE,GAAG,SAAS,EACnD,cAAc,EAAE,MAAM,GACrB,gBAAgB,EAAE,CASpB;AAED,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,sBAAsB,EAAE,GAAG,SAAS,EACnD,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,GACZ,gBAAgB,GAAG,SAAS,CAG9B;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,GAAG,SAAS,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAMrC"}
@@ -0,0 +1,19 @@
1
+ import { isRecord, } from '@syncular/core';
2
+ export function subscriptionChanges(subscriptions, subscriptionId) {
3
+ const subscription = subscriptions?.find((item) => item.id === subscriptionId);
4
+ if (!subscription) {
5
+ return [];
6
+ }
7
+ return subscription.commits?.flatMap((commit) => commit.changes) ?? [];
8
+ }
9
+ export function findSubscriptionChange(subscriptions, subscriptionId, rowId) {
10
+ const changes = subscriptionChanges(subscriptions, subscriptionId);
11
+ return changes.find((change) => change.row_id === rowId);
12
+ }
13
+ export function subscriptionChangeRow(change) {
14
+ if (!change) {
15
+ return undefined;
16
+ }
17
+ return isRecord(change.row_json) ? change.row_json : undefined;
18
+ }
19
+ //# sourceMappingURL=sync-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-response.js","sourceRoot":"","sources":["../src/sync-response.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,GAGT,MAAM,gBAAgB,CAAC;AASxB,MAAM,UAAU,mBAAmB,CACjC,aAAmD,EACnD,cAAsB,EACF;IACpB,MAAM,YAAY,GAAG,aAAa,EAAE,IAAI,CACtC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,cAAc,CACrC,CAAC;IACF,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;AAAA,CACxE;AAED,MAAM,UAAU,sBAAsB,CACpC,aAAmD,EACnD,cAAsB,EACtB,KAAa,EACiB;IAC9B,MAAM,OAAO,GAAG,mBAAmB,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IACnE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC;AAAA,CAC1D;AAED,MAAM,UAAU,qBAAqB,CACnC,MAAoC,EACC;IACrC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CAChE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/testkit",
3
- "version": "0.0.0",
3
+ "version": "0.0.2-136",
4
4
  "description": "Testing fixtures and utilities for Syncular",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Kniffler",
@@ -41,17 +41,17 @@
41
41
  "release": "bunx syncular-publish"
42
42
  },
43
43
  "dependencies": {
44
- "@syncular/client": "0.0.0",
45
- "@syncular/core": "0.0.0",
46
- "@syncular/dialect-bun-sqlite": "0.0.0",
47
- "@syncular/dialect-libsql": "0.0.0",
48
- "@syncular/dialect-pglite": "0.0.0",
49
- "@syncular/dialect-sqlite3": "0.0.0",
50
- "@syncular/server": "0.0.0",
51
- "@syncular/server-dialect-postgres": "0.0.0",
52
- "@syncular/server-dialect-sqlite": "0.0.0",
53
- "@syncular/server-hono": "0.0.0",
54
- "@syncular/transport-http": "0.0.0",
44
+ "@syncular/client": "0.0.2-136",
45
+ "@syncular/core": "0.0.2-136",
46
+ "@syncular/dialect-bun-sqlite": "0.0.2-136",
47
+ "@syncular/dialect-libsql": "0.0.2-136",
48
+ "@syncular/dialect-pglite": "0.0.2-136",
49
+ "@syncular/dialect-sqlite3": "0.0.2-136",
50
+ "@syncular/server": "0.0.2-136",
51
+ "@syncular/server-dialect-postgres": "0.0.2-136",
52
+ "@syncular/server-dialect-sqlite": "0.0.2-136",
53
+ "@syncular/server-hono": "0.0.2-136",
54
+ "@syncular/transport-http": "0.0.2-136",
55
55
  "hono": "^4.11.9"
56
56
  },
57
57
  "devDependencies": {
package/src/faults.ts CHANGED
@@ -224,6 +224,3 @@ export function withRecording(
224
224
  },
225
225
  };
226
226
  }
227
-
228
- export const createErrorTransport = withFaults;
229
- export const createRecordingTransport = withRecording;
package/src/fixtures.ts CHANGED
@@ -844,6 +844,3 @@ export async function destroyTestServer(
844
844
  ): Promise<void> {
845
845
  await server.destroy();
846
846
  }
847
-
848
- export const createServerFixture = createTestServer;
849
- export const createClientFixture = createTestClient;
package/src/index.ts CHANGED
@@ -4,3 +4,6 @@ export * from './fixtures';
4
4
  export * from './hono-node-server';
5
5
  export * from './http-fixtures';
6
6
  export * from './project-scoped-tasks';
7
+ export * from './runtime-process';
8
+ export * from './sync-http';
9
+ export * from './sync-response';
@@ -1,4 +1,8 @@
1
- import { isRecord, type SyncOperation } from '@syncular/core';
1
+ import {
2
+ isRecord,
3
+ type SyncOperation,
4
+ type SyncSubscriptionRequest,
5
+ } from '@syncular/core';
2
6
  import type {
3
7
  ApplyOperationResult,
4
8
  EmittedChange,
@@ -297,3 +301,49 @@ export function createProjectScopedTasksHandler<
297
301
  },
298
302
  };
299
303
  }
304
+
305
+ export interface CreateProjectScopedTasksSubscriptionOptions {
306
+ id?: string;
307
+ userId: string;
308
+ projectId?: string;
309
+ cursor?: number;
310
+ }
311
+
312
+ export function createProjectScopedTasksSubscription(
313
+ options: CreateProjectScopedTasksSubscriptionOptions
314
+ ): SyncSubscriptionRequest {
315
+ return {
316
+ id: options.id ?? 'sub-tasks',
317
+ table: 'tasks',
318
+ scopes: {
319
+ user_id: options.userId,
320
+ project_id: options.projectId ?? 'p0',
321
+ },
322
+ cursor: options.cursor ?? 0,
323
+ bootstrapState: null,
324
+ };
325
+ }
326
+
327
+ export interface CreateProjectScopedTaskUpsertOperationOptions {
328
+ taskId: string;
329
+ title: string;
330
+ completed?: number;
331
+ projectId?: string;
332
+ baseVersion?: number | null;
333
+ }
334
+
335
+ export function createProjectScopedTaskUpsertOperation(
336
+ options: CreateProjectScopedTaskUpsertOperationOptions
337
+ ): SyncOperation {
338
+ return {
339
+ table: 'tasks',
340
+ row_id: options.taskId,
341
+ op: 'upsert',
342
+ payload: {
343
+ title: options.title,
344
+ completed: options.completed ?? 0,
345
+ project_id: options.projectId ?? 'p0',
346
+ },
347
+ base_version: options.baseVersion ?? null,
348
+ };
349
+ }
@@ -0,0 +1,133 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
3
+ export interface WaitForJsonPortOptions {
4
+ timeoutMs?: number;
5
+ processName?: string;
6
+ }
7
+
8
+ export async function waitForJsonPortFromStdout(
9
+ process: ChildProcess,
10
+ options: WaitForJsonPortOptions = {}
11
+ ): Promise<number> {
12
+ const timeoutMs = options.timeoutMs ?? 30_000;
13
+ const processName = options.processName ?? 'Child process';
14
+
15
+ return new Promise<number>((resolve, reject) => {
16
+ const stdout = process.stdout;
17
+ const stderr = process.stderr;
18
+
19
+ if (!stdout || !stderr) {
20
+ reject(new Error(`${processName} has no stdout/stderr pipes`));
21
+ return;
22
+ }
23
+
24
+ let stdoutBuffer = '';
25
+ let stderrBuffer = '';
26
+
27
+ const cleanup = () => {
28
+ clearTimeout(timeout);
29
+ stdout.off('data', onStdoutData);
30
+ stderr.off('data', onStderrData);
31
+ process.off('exit', onExit);
32
+ };
33
+
34
+ const resolvePort = (port: number) => {
35
+ cleanup();
36
+ resolve(port);
37
+ };
38
+
39
+ const rejectWith = (error: Error) => {
40
+ cleanup();
41
+ reject(error);
42
+ };
43
+
44
+ const onStdoutData = (chunk: Buffer | string) => {
45
+ stdoutBuffer += chunk.toString();
46
+
47
+ const lines = stdoutBuffer.split('\n');
48
+ stdoutBuffer = lines.pop() ?? '';
49
+
50
+ for (const line of lines) {
51
+ const trimmed = line.trim();
52
+ if (!trimmed) {
53
+ continue;
54
+ }
55
+
56
+ try {
57
+ const parsed = JSON.parse(trimmed) as { port?: number };
58
+ if (parsed.port && parsed.port > 0) {
59
+ resolvePort(parsed.port);
60
+ return;
61
+ }
62
+ } catch {
63
+ // keep reading until a valid JSON line is emitted
64
+ }
65
+ }
66
+ };
67
+
68
+ const onStderrData = (chunk: Buffer | string) => {
69
+ stderrBuffer += chunk.toString();
70
+ };
71
+
72
+ const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
73
+ rejectWith(
74
+ new Error(
75
+ `${processName} exited before reporting port (code=${String(code)} signal=${String(signal)})\nstderr: ${stderrBuffer}`
76
+ )
77
+ );
78
+ };
79
+
80
+ const timeout = setTimeout(() => {
81
+ rejectWith(
82
+ new Error(
83
+ `${processName} startup timed out after ${timeoutMs}ms\nstderr: ${stderrBuffer}`
84
+ )
85
+ );
86
+ }, timeoutMs);
87
+
88
+ stdout.on('data', onStdoutData);
89
+ stderr.on('data', onStderrData);
90
+ process.on('exit', onExit);
91
+ });
92
+ }
93
+
94
+ export interface StopChildProcessOptions {
95
+ gracePeriodMs?: number;
96
+ }
97
+
98
+ export async function stopChildProcess(
99
+ process: ChildProcess,
100
+ options: StopChildProcessOptions = {}
101
+ ): Promise<void> {
102
+ if (process.exitCode != null) {
103
+ return;
104
+ }
105
+
106
+ const gracePeriodMs = options.gracePeriodMs ?? 5000;
107
+
108
+ try {
109
+ process.kill('SIGTERM');
110
+ } catch {
111
+ return;
112
+ }
113
+
114
+ await new Promise<void>((resolve) => {
115
+ const onExit = () => {
116
+ clearTimeout(timeout);
117
+ process.off('exit', onExit);
118
+ resolve();
119
+ };
120
+
121
+ const timeout = setTimeout(() => {
122
+ try {
123
+ process.kill('SIGKILL');
124
+ } catch {
125
+ // ignore
126
+ }
127
+ process.off('exit', onExit);
128
+ resolve();
129
+ }, gracePeriodMs);
130
+
131
+ process.on('exit', onExit);
132
+ });
133
+ }
@@ -0,0 +1,100 @@
1
+ import type {
2
+ SyncCombinedRequest,
3
+ SyncCombinedResponse,
4
+ SyncPullRequest,
5
+ SyncPullResponse,
6
+ SyncPushRequest,
7
+ SyncPushResponse,
8
+ } from '@syncular/core';
9
+
10
+ export interface JsonActorHeadersOptions {
11
+ actorId: string;
12
+ actorHeader?: string;
13
+ extraHeaders?: Record<string, string>;
14
+ }
15
+
16
+ export function createJsonActorHeaders(
17
+ options: JsonActorHeadersOptions
18
+ ): Record<string, string> {
19
+ return {
20
+ 'content-type': 'application/json',
21
+ [options.actorHeader ?? 'x-actor-id']: options.actorId,
22
+ ...options.extraHeaders,
23
+ };
24
+ }
25
+
26
+ export interface PostJsonWithActorOptions<TBody> {
27
+ fetch: typeof globalThis.fetch;
28
+ url: string;
29
+ actorId: string;
30
+ actorHeader?: string;
31
+ extraHeaders?: Record<string, string>;
32
+ body: TBody;
33
+ }
34
+
35
+ export interface PostJsonWithActorResult<TResponse> {
36
+ response: Response;
37
+ json: TResponse;
38
+ }
39
+
40
+ export async function postJsonWithActor<TBody, TResponse>(
41
+ options: PostJsonWithActorOptions<TBody>
42
+ ): Promise<PostJsonWithActorResult<TResponse>> {
43
+ const response = await options.fetch(options.url, {
44
+ method: 'POST',
45
+ headers: createJsonActorHeaders({
46
+ actorId: options.actorId,
47
+ actorHeader: options.actorHeader,
48
+ extraHeaders: options.extraHeaders,
49
+ }),
50
+ body: JSON.stringify(options.body),
51
+ });
52
+
53
+ const json = (await response.json()) as TResponse;
54
+ return { response, json };
55
+ }
56
+
57
+ export interface PostSyncCombinedRequestOptions {
58
+ fetch: typeof globalThis.fetch;
59
+ url: string;
60
+ actorId: string;
61
+ actorHeader?: string;
62
+ extraHeaders?: Record<string, string>;
63
+ body: SyncCombinedRequest;
64
+ }
65
+
66
+ export async function postSyncCombinedRequest(
67
+ options: PostSyncCombinedRequestOptions
68
+ ): Promise<PostJsonWithActorResult<SyncCombinedResponse>> {
69
+ return postJsonWithActor<SyncCombinedRequest, SyncCombinedResponse>(options);
70
+ }
71
+
72
+ export interface PostSyncPushRequestOptions {
73
+ fetch: typeof globalThis.fetch;
74
+ url: string;
75
+ actorId: string;
76
+ actorHeader?: string;
77
+ extraHeaders?: Record<string, string>;
78
+ body: SyncPushRequest;
79
+ }
80
+
81
+ export async function postSyncPushRequest(
82
+ options: PostSyncPushRequestOptions
83
+ ): Promise<PostJsonWithActorResult<SyncPushResponse>> {
84
+ return postJsonWithActor<SyncPushRequest, SyncPushResponse>(options);
85
+ }
86
+
87
+ export interface PostSyncPullRequestOptions {
88
+ fetch: typeof globalThis.fetch;
89
+ url: string;
90
+ actorId: string;
91
+ actorHeader?: string;
92
+ extraHeaders?: Record<string, string>;
93
+ body: SyncPullRequest;
94
+ }
95
+
96
+ export async function postSyncPullRequest(
97
+ options: PostSyncPullRequestOptions
98
+ ): Promise<PostJsonWithActorResult<SyncPullResponse>> {
99
+ return postJsonWithActor<SyncPullRequest, SyncPullResponse>(options);
100
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ isRecord,
3
+ type SyncChange,
4
+ type SyncPullResponse,
5
+ } from '@syncular/core';
6
+
7
+ export type SyncChangeRecord = SyncChange;
8
+
9
+ export type SyncSubscriptionRecord = Pick<
10
+ SyncPullResponse['subscriptions'][number],
11
+ 'id' | 'commits'
12
+ >;
13
+
14
+ export function subscriptionChanges(
15
+ subscriptions: SyncSubscriptionRecord[] | undefined,
16
+ subscriptionId: string
17
+ ): SyncChangeRecord[] {
18
+ const subscription = subscriptions?.find(
19
+ (item) => item.id === subscriptionId
20
+ );
21
+ if (!subscription) {
22
+ return [];
23
+ }
24
+
25
+ return subscription.commits?.flatMap((commit) => commit.changes) ?? [];
26
+ }
27
+
28
+ export function findSubscriptionChange(
29
+ subscriptions: SyncSubscriptionRecord[] | undefined,
30
+ subscriptionId: string,
31
+ rowId: string
32
+ ): SyncChangeRecord | undefined {
33
+ const changes = subscriptionChanges(subscriptions, subscriptionId);
34
+ return changes.find((change) => change.row_id === rowId);
35
+ }
36
+
37
+ export function subscriptionChangeRow(
38
+ change: SyncChangeRecord | undefined
39
+ ): Record<string, unknown> | undefined {
40
+ if (!change) {
41
+ return undefined;
42
+ }
43
+
44
+ return isRecord(change.row_json) ? change.row_json : undefined;
45
+ }