@syncular/relay 0.0.1-60

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 (68) hide show
  1. package/dist/client-role/forward-engine.d.ts +63 -0
  2. package/dist/client-role/forward-engine.d.ts.map +1 -0
  3. package/dist/client-role/forward-engine.js +263 -0
  4. package/dist/client-role/forward-engine.js.map +1 -0
  5. package/dist/client-role/index.d.ts +9 -0
  6. package/dist/client-role/index.d.ts.map +1 -0
  7. package/dist/client-role/index.js +9 -0
  8. package/dist/client-role/index.js.map +1 -0
  9. package/dist/client-role/pull-engine.d.ts +70 -0
  10. package/dist/client-role/pull-engine.d.ts.map +1 -0
  11. package/dist/client-role/pull-engine.js +233 -0
  12. package/dist/client-role/pull-engine.js.map +1 -0
  13. package/dist/client-role/sequence-mapper.d.ts +65 -0
  14. package/dist/client-role/sequence-mapper.d.ts.map +1 -0
  15. package/dist/client-role/sequence-mapper.js +161 -0
  16. package/dist/client-role/sequence-mapper.js.map +1 -0
  17. package/dist/index.d.ts +37 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +44 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/migrate.d.ts +18 -0
  22. package/dist/migrate.d.ts.map +1 -0
  23. package/dist/migrate.js +99 -0
  24. package/dist/migrate.js.map +1 -0
  25. package/dist/mode-manager.d.ts +60 -0
  26. package/dist/mode-manager.d.ts.map +1 -0
  27. package/dist/mode-manager.js +114 -0
  28. package/dist/mode-manager.js.map +1 -0
  29. package/dist/realtime.d.ts +102 -0
  30. package/dist/realtime.d.ts.map +1 -0
  31. package/dist/realtime.js +305 -0
  32. package/dist/realtime.js.map +1 -0
  33. package/dist/relay.d.ts +188 -0
  34. package/dist/relay.d.ts.map +1 -0
  35. package/dist/relay.js +315 -0
  36. package/dist/relay.js.map +1 -0
  37. package/dist/schema.d.ts +158 -0
  38. package/dist/schema.d.ts.map +1 -0
  39. package/dist/schema.js +7 -0
  40. package/dist/schema.js.map +1 -0
  41. package/dist/server-role/index.d.ts +54 -0
  42. package/dist/server-role/index.d.ts.map +1 -0
  43. package/dist/server-role/index.js +198 -0
  44. package/dist/server-role/index.js.map +1 -0
  45. package/dist/server-role/pull.d.ts +25 -0
  46. package/dist/server-role/pull.d.ts.map +1 -0
  47. package/dist/server-role/pull.js +24 -0
  48. package/dist/server-role/pull.js.map +1 -0
  49. package/dist/server-role/push.d.ts +27 -0
  50. package/dist/server-role/push.d.ts.map +1 -0
  51. package/dist/server-role/push.js +98 -0
  52. package/dist/server-role/push.js.map +1 -0
  53. package/package.json +61 -0
  54. package/src/__tests__/relay.test.ts +464 -0
  55. package/src/bun-types.d.ts +50 -0
  56. package/src/client-role/forward-engine.ts +352 -0
  57. package/src/client-role/index.ts +9 -0
  58. package/src/client-role/pull-engine.ts +301 -0
  59. package/src/client-role/sequence-mapper.ts +201 -0
  60. package/src/index.ts +50 -0
  61. package/src/migrate.ts +113 -0
  62. package/src/mode-manager.ts +142 -0
  63. package/src/realtime.ts +370 -0
  64. package/src/relay.ts +421 -0
  65. package/src/schema.ts +171 -0
  66. package/src/server-role/index.ts +342 -0
  67. package/src/server-role/pull.ts +37 -0
  68. package/src/server-role/push.ts +130 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @syncular/relay - Server Role
3
+ *
4
+ * Hono routes for serving local clients.
5
+ */
6
+ import { createSyncTimer, logSyncEvent, ScopeValuesSchema, SyncBootstrapStateSchema, SyncPullRequestSchema, SyncPushRequestSchema, } from '@syncular/core';
7
+ import { recordClientCursor } from '@syncular/server';
8
+ import { Hono } from 'hono';
9
+ import { relayPull } from './pull';
10
+ import { relayPushCommit } from './push';
11
+ function clampInt(value, min, max) {
12
+ return Math.max(min, Math.min(max, value));
13
+ }
14
+ function isRecord(value) {
15
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ }
17
+ /**
18
+ * Create Hono routes for relay server-role endpoints.
19
+ *
20
+ * Provides:
21
+ * - POST /pull (commit stream + optional bootstrap snapshots)
22
+ * - POST /push (commit ingestion)
23
+ * - GET /realtime (WebSocket wake-up notifications)
24
+ */
25
+ export function createRelayRoutes(options) {
26
+ const routes = new Hono();
27
+ const maxOperationsPerPush = options.maxOperationsPerPush ?? 200;
28
+ const maxSubscriptionsPerPull = options.maxSubscriptionsPerPull ?? 200;
29
+ const maxPullLimitCommits = options.maxPullLimitCommits ?? 100;
30
+ const authenticate = options.authenticate ?? (async () => ({ actorId: 'anonymous' }));
31
+ // POST /pull
32
+ routes.post('/pull', async (c) => {
33
+ const auth = await authenticate(c);
34
+ if (!auth)
35
+ return c.json({ error: 'UNAUTHENTICATED' }, 401);
36
+ const rawBody = await c.req.json();
37
+ const timer = createSyncTimer();
38
+ if (!isRecord(rawBody)) {
39
+ return c.json({ error: 'INVALID_REQUEST', message: 'Invalid JSON body' }, 400);
40
+ }
41
+ if (!rawBody.clientId || typeof rawBody.clientId !== 'string') {
42
+ return c.json({ error: 'INVALID_REQUEST', message: 'clientId is required' }, 400);
43
+ }
44
+ if (!Array.isArray(rawBody.subscriptions)) {
45
+ return c.json({
46
+ error: 'INVALID_REQUEST',
47
+ message: 'subscriptions array is required',
48
+ }, 400);
49
+ }
50
+ if (rawBody.subscriptions.length > maxSubscriptionsPerPull) {
51
+ return c.json({
52
+ error: 'INVALID_REQUEST',
53
+ message: `Too many subscriptions (max ${maxSubscriptionsPerPull})`,
54
+ }, 400);
55
+ }
56
+ const subscriptions = [];
57
+ for (const subValue of rawBody.subscriptions) {
58
+ if (!isRecord(subValue)) {
59
+ return c.json({ error: 'INVALID_REQUEST', message: 'Invalid subscription entry' }, 400);
60
+ }
61
+ const id = typeof subValue.id === 'string' ? subValue.id : null;
62
+ const shape = typeof subValue.shape === 'string' ? subValue.shape : null;
63
+ if (!id || !shape) {
64
+ return c.json({
65
+ error: 'INVALID_REQUEST',
66
+ message: 'Subscription id/shape required',
67
+ }, 400);
68
+ }
69
+ const scopesParsed = ScopeValuesSchema.safeParse(subValue.scopes);
70
+ if (!scopesParsed.success) {
71
+ return c.json({ error: 'INVALID_REQUEST', message: 'Invalid subscription scopes' }, 400);
72
+ }
73
+ const rawParams = subValue.params;
74
+ if (rawParams !== undefined && !isRecord(rawParams)) {
75
+ return c.json({ error: 'INVALID_REQUEST', message: 'Invalid subscription params' }, 400);
76
+ }
77
+ const cursor = typeof subValue.cursor === 'number' && Number.isInteger(subValue.cursor)
78
+ ? Math.max(-1, subValue.cursor)
79
+ : -1;
80
+ const rawBootstrapState = subValue.bootstrapState;
81
+ const bootstrapState = rawBootstrapState === undefined || rawBootstrapState === null
82
+ ? null
83
+ : (() => {
84
+ const parsed = SyncBootstrapStateSchema.safeParse(rawBootstrapState);
85
+ if (!parsed.success)
86
+ return null;
87
+ return parsed.data;
88
+ })();
89
+ if (rawBootstrapState !== undefined &&
90
+ rawBootstrapState !== null &&
91
+ bootstrapState === null) {
92
+ return c.json({
93
+ error: 'INVALID_REQUEST',
94
+ message: 'Invalid subscription bootstrapState',
95
+ }, 400);
96
+ }
97
+ subscriptions.push({
98
+ id,
99
+ shape,
100
+ scopes: scopesParsed.data,
101
+ params: rawParams,
102
+ cursor,
103
+ bootstrapState,
104
+ });
105
+ }
106
+ const request = {
107
+ clientId: rawBody.clientId,
108
+ limitCommits: clampInt(typeof rawBody.limitCommits === 'number' &&
109
+ Number.isInteger(rawBody.limitCommits)
110
+ ? rawBody.limitCommits
111
+ : 50, 1, maxPullLimitCommits),
112
+ limitSnapshotRows: clampInt(typeof rawBody.limitSnapshotRows === 'number' &&
113
+ Number.isInteger(rawBody.limitSnapshotRows)
114
+ ? rawBody.limitSnapshotRows
115
+ : 1000, 1, 5000),
116
+ maxSnapshotPages: clampInt(typeof rawBody.maxSnapshotPages === 'number' &&
117
+ Number.isInteger(rawBody.maxSnapshotPages)
118
+ ? rawBody.maxSnapshotPages
119
+ : 1, 1, 10),
120
+ dedupeRows: typeof rawBody.dedupeRows === 'boolean'
121
+ ? rawBody.dedupeRows
122
+ : undefined,
123
+ subscriptions,
124
+ };
125
+ const validatedRequest = SyncPullRequestSchema.safeParse(request);
126
+ if (!validatedRequest.success) {
127
+ return c.json({ error: 'INVALID_REQUEST', message: 'Invalid pull request' }, 400);
128
+ }
129
+ const pullResult = await relayPull({
130
+ db: options.db,
131
+ dialect: options.dialect,
132
+ shapes: options.shapes,
133
+ actorId: auth.actorId,
134
+ request: validatedRequest.data,
135
+ });
136
+ await recordClientCursor(options.db, options.dialect, {
137
+ clientId: validatedRequest.data.clientId,
138
+ actorId: auth.actorId,
139
+ cursor: pullResult.clientCursor,
140
+ effectiveScopes: pullResult.effectiveScopes,
141
+ });
142
+ // Notify realtime about updated scope values
143
+ const tables = Object.keys(pullResult.effectiveScopes);
144
+ options.realtime.updateClientTables(request.clientId, tables);
145
+ logSyncEvent({
146
+ event: 'relay.pull',
147
+ userId: auth.actorId,
148
+ durationMs: timer(),
149
+ subscriptionCount: pullResult.response.subscriptions.length,
150
+ clientCursor: pullResult.clientCursor,
151
+ });
152
+ return c.json(pullResult.response);
153
+ });
154
+ // POST /push
155
+ routes.post('/push', async (c) => {
156
+ const auth = await authenticate(c);
157
+ if (!auth)
158
+ return c.json({ error: 'UNAUTHENTICATED' }, 401);
159
+ const rawBody = await c.req.json();
160
+ const parsed = SyncPushRequestSchema.safeParse(rawBody);
161
+ if (!parsed.success) {
162
+ return c.json({ error: 'INVALID_REQUEST', message: 'Invalid push request' }, 400);
163
+ }
164
+ const body = parsed.data;
165
+ if (body.operations.length > maxOperationsPerPush) {
166
+ return c.json({
167
+ error: 'TOO_MANY_OPERATIONS',
168
+ message: `Maximum ${maxOperationsPerPush} operations per push`,
169
+ }, 400);
170
+ }
171
+ const timer = createSyncTimer();
172
+ const pushed = await relayPushCommit({
173
+ db: options.db,
174
+ dialect: options.dialect,
175
+ shapes: options.shapes,
176
+ actorId: auth.actorId,
177
+ request: body,
178
+ });
179
+ logSyncEvent({
180
+ event: 'relay.push',
181
+ userId: auth.actorId,
182
+ durationMs: timer(),
183
+ operationCount: body.operations.length,
184
+ status: pushed.response.status,
185
+ commitSeq: pushed.response.commitSeq,
186
+ });
187
+ // Notify about the commit
188
+ if (pushed.response.ok === true &&
189
+ pushed.response.status === 'applied' &&
190
+ typeof pushed.response.commitSeq === 'number' &&
191
+ pushed.affectedTables.length > 0) {
192
+ await options.onCommit?.(pushed.response.commitSeq, pushed.affectedTables);
193
+ }
194
+ return c.json(pushed.response);
195
+ });
196
+ return routes;
197
+ }
198
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server-role/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @syncular/relay - Server Role Pull Handler
3
+ *
4
+ * Handles pulls from local clients, serving changes stored locally
5
+ * on the relay.
6
+ */
7
+ import type { SyncPullRequest } from '@syncular/core';
8
+ import type { ServerSyncDialect, TableRegistry } from '@syncular/server';
9
+ import { type PullResult } from '@syncular/server';
10
+ import type { Kysely } from 'kysely';
11
+ import type { RelayDatabase } from '../schema';
12
+ /**
13
+ * Pull commits for a local client from the relay.
14
+ *
15
+ * This wraps the standard server pull with relay-specific logic:
16
+ * - Restricts scopes to those the relay subscribes to
17
+ */
18
+ export declare function relayPull<DB extends RelayDatabase = RelayDatabase>(args: {
19
+ db: Kysely<DB>;
20
+ dialect: ServerSyncDialect;
21
+ shapes: TableRegistry<DB>;
22
+ actorId: string;
23
+ request: SyncPullRequest;
24
+ }): Promise<PullResult>;
25
+ //# sourceMappingURL=pull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/server-role/pull.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,KAAK,UAAU,EAAQ,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,EAAE,SAAS,aAAa,GAAG,aAAa,EACxC,IAAI,EAAE;IACN,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;CAC1B,GAAG,OAAO,CAAC,UAAU,CAAC,CAStB"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @syncular/relay - Server Role Pull Handler
3
+ *
4
+ * Handles pulls from local clients, serving changes stored locally
5
+ * on the relay.
6
+ */
7
+ import { pull } from '@syncular/server';
8
+ /**
9
+ * Pull commits for a local client from the relay.
10
+ *
11
+ * This wraps the standard server pull with relay-specific logic:
12
+ * - Restricts scopes to those the relay subscribes to
13
+ */
14
+ export async function relayPull(args) {
15
+ // Use the standard pull - scope authorization is handled by shapes
16
+ return pull({
17
+ db: args.db,
18
+ dialect: args.dialect,
19
+ shapes: args.shapes,
20
+ actorId: args.actorId,
21
+ request: args.request,
22
+ });
23
+ }
24
+ //# sourceMappingURL=pull.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull.js","sourceRoot":"","sources":["../../src/server-role/pull.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAmB,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIzD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAE7B,IAMD,EAAuB;IACtB,mEAAmE;IACnE,OAAO,IAAI,CAAC;QACV,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;AAAA,CACJ"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @syncular/relay - Server Role Push Handler
3
+ *
4
+ * Handles pushes from local clients, storing them locally and
5
+ * queueing them for forwarding to the main server.
6
+ */
7
+ import type { SyncPushRequest } from '@syncular/core';
8
+ import type { ServerSyncDialect, TableRegistry } from '@syncular/server';
9
+ import { type PushCommitResult } from '@syncular/server';
10
+ import { type Kysely } from 'kysely';
11
+ import type { RelayDatabase } from '../schema';
12
+ /**
13
+ * Push a commit from a local client to the relay.
14
+ *
15
+ * This wraps the standard server pushCommit with relay-specific logic:
16
+ * 1. Validates that operations are within the relay's scope
17
+ * 2. Stores the commit locally
18
+ * 3. Enqueues the commit for forwarding to the main server
19
+ */
20
+ export declare function relayPushCommit<DB extends RelayDatabase = RelayDatabase>(args: {
21
+ db: Kysely<DB>;
22
+ dialect: ServerSyncDialect;
23
+ shapes: TableRegistry<DB>;
24
+ actorId: string;
25
+ request: SyncPushRequest;
26
+ }): Promise<PushCommitResult>;
27
+ //# sourceMappingURL=push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/server-role/push.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,KAAK,gBAAgB,EAAc,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAY/C;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,EAAE,SAAS,aAAa,GAAG,aAAa,EACxC,IAAI,EAAE;IACN,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,eAAe,CAAC;CAC1B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4B5B"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @syncular/relay - Server Role Push Handler
3
+ *
4
+ * Handles pushes from local clients, storing them locally and
5
+ * queueing them for forwarding to the main server.
6
+ */
7
+ import { pushCommit } from '@syncular/server';
8
+ import { sql } from 'kysely';
9
+ function randomId() {
10
+ if (typeof crypto !== 'undefined' &&
11
+ typeof crypto.randomUUID === 'function') {
12
+ return crypto.randomUUID();
13
+ }
14
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
15
+ }
16
+ /**
17
+ * Push a commit from a local client to the relay.
18
+ *
19
+ * This wraps the standard server pushCommit with relay-specific logic:
20
+ * 1. Validates that operations are within the relay's scope
21
+ * 2. Stores the commit locally
22
+ * 3. Enqueues the commit for forwarding to the main server
23
+ */
24
+ export async function relayPushCommit(args) {
25
+ const { request } = args;
26
+ // Use the standard pushCommit - scope authorization is handled by shapes
27
+ const result = await pushCommit({
28
+ db: args.db,
29
+ dialect: args.dialect,
30
+ shapes: args.shapes,
31
+ actorId: args.actorId,
32
+ request,
33
+ });
34
+ // If the commit was applied, enqueue it for forwarding to main server
35
+ if (result.response.ok === true &&
36
+ result.response.status === 'applied' &&
37
+ typeof result.response.commitSeq === 'number') {
38
+ await enqueueForForwarding(args.db, {
39
+ localCommitSeq: result.response.commitSeq,
40
+ clientId: request.clientId,
41
+ clientCommitId: request.clientCommitId,
42
+ operations: request.operations,
43
+ schemaVersion: request.schemaVersion,
44
+ });
45
+ }
46
+ return result;
47
+ }
48
+ /**
49
+ * Enqueue a locally-committed change for forwarding to the main server.
50
+ */
51
+ async function enqueueForForwarding(db, args) {
52
+ const now = Date.now();
53
+ await sql `
54
+ insert into ${sql.table('relay_forward_outbox')} (
55
+ id,
56
+ local_commit_seq,
57
+ client_id,
58
+ client_commit_id,
59
+ operations_json,
60
+ schema_version,
61
+ status,
62
+ main_commit_seq,
63
+ error,
64
+ last_response_json,
65
+ created_at,
66
+ updated_at,
67
+ attempt_count
68
+ )
69
+ values (
70
+ ${randomId()},
71
+ ${args.localCommitSeq},
72
+ ${args.clientId},
73
+ ${args.clientCommitId},
74
+ ${JSON.stringify(args.operations)},
75
+ ${args.schemaVersion},
76
+ 'pending',
77
+ ${null},
78
+ ${null},
79
+ ${null},
80
+ ${now},
81
+ ${now},
82
+ ${0}
83
+ )
84
+ `.execute(db);
85
+ // Also create a sequence map entry for this commit
86
+ await sql `
87
+ insert into ${sql.table('relay_sequence_map')} (
88
+ local_commit_seq,
89
+ main_commit_seq,
90
+ status,
91
+ created_at,
92
+ updated_at
93
+ )
94
+ values (${args.localCommitSeq}, ${null}, 'pending', ${now}, ${now})
95
+ on conflict (local_commit_seq) do nothing
96
+ `.execute(db);
97
+ }
98
+ //# sourceMappingURL=push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/server-role/push.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAyB,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAe,GAAG,EAAE,MAAM,QAAQ,CAAC;AAG1C,SAAS,QAAQ,GAAW;IAC1B,IACE,OAAO,MAAM,KAAK,WAAW;QAC7B,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,EACvC,CAAC;QACD,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CAC/D;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAEnC,IAMD,EAA6B;IAC5B,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAEzB,yEAAyE;IACzE,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC;QAC9B,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,OAAO;KACR,CAAC,CAAC;IAEH,sEAAsE;IACtE,IACE,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI;QAC3B,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS;QACpC,OAAO,MAAM,CAAC,QAAQ,CAAC,SAAS,KAAK,QAAQ,EAC7C,CAAC;QACD,MAAM,oBAAoB,CAAC,IAAI,CAAC,EAAE,EAAE;YAClC,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS;YACzC,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,aAAa,EAAE,OAAO,CAAC,aAAa;SACrC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACf;AAED;;GAEG;AACH,KAAK,UAAU,oBAAoB,CACjC,EAAc,EACd,IAMC,EACc;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,MAAM,GAAG,CAAA;kBACO,GAAG,CAAC,KAAK,CAAC,sBAAsB,CAAC;;;;;;;;;;;;;;;;QAgB3C,QAAQ,EAAE;QACV,IAAI,CAAC,cAAc;QACnB,IAAI,CAAC,QAAQ;QACb,IAAI,CAAC,cAAc;QACnB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC;QAC/B,IAAI,CAAC,aAAa;;QAElB,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,GAAG;QACH,GAAG;QACH,CAAC;;GAEN,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAEd,mDAAmD;IACnD,MAAM,GAAG,CAAA;kBACO,GAAG,CAAC,KAAK,CAAC,oBAAoB,CAAC;;;;;;;cAOnC,IAAI,CAAC,cAAc,KAAK,IAAI,gBAAgB,GAAG,KAAK,GAAG;;GAElE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAAA,CACf"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@syncular/relay",
3
+ "version": "0.0.1-60",
4
+ "description": "Edge relay for Syncular distributed sync",
5
+ "license": "MIT",
6
+ "author": "Benjamin Kniffler",
7
+ "homepage": "https://syncular.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/syncular/syncular.git",
11
+ "directory": "packages/relay"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/syncular/syncular/issues"
15
+ },
16
+ "keywords": [
17
+ "sync",
18
+ "offline-first",
19
+ "realtime",
20
+ "database",
21
+ "typescript",
22
+ "relay",
23
+ "edge"
24
+ ],
25
+ "private": false,
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "bun": "./src/index.ts",
33
+ "import": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ }
37
+ }
38
+ },
39
+ "scripts": {
40
+ "tsgo": "tsgo --noEmit",
41
+ "build": "rm -rf dist && tsgo",
42
+ "release": "bun pm pack --destination . && npm publish ./*.tgz --tag latest && rm -f ./*.tgz"
43
+ },
44
+ "dependencies": {
45
+ "@syncular/core": "0.0.1",
46
+ "@syncular/server": "0.0.1"
47
+ },
48
+ "peerDependencies": {
49
+ "hono": "^4.0.0",
50
+ "kysely": "^0.28.0"
51
+ },
52
+ "devDependencies": {
53
+ "@syncular/config": "0.0.0",
54
+ "@syncular/server-dialect-sqlite": "0.0.1",
55
+ "kysely": "*"
56
+ },
57
+ "files": [
58
+ "dist",
59
+ "src"
60
+ ]
61
+ }