@syncular/core 0.0.1-100

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 (99) hide show
  1. package/dist/blobs.d.ts +146 -0
  2. package/dist/blobs.d.ts.map +1 -0
  3. package/dist/blobs.js +47 -0
  4. package/dist/blobs.js.map +1 -0
  5. package/dist/conflict.d.ts +22 -0
  6. package/dist/conflict.d.ts.map +1 -0
  7. package/dist/conflict.js +81 -0
  8. package/dist/conflict.js.map +1 -0
  9. package/dist/index.d.ts +24 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/kysely-serialize.d.ts +22 -0
  14. package/dist/kysely-serialize.d.ts.map +1 -0
  15. package/dist/kysely-serialize.js +147 -0
  16. package/dist/kysely-serialize.js.map +1 -0
  17. package/dist/logger.d.ts +29 -0
  18. package/dist/logger.d.ts.map +1 -0
  19. package/dist/logger.js +26 -0
  20. package/dist/logger.js.map +1 -0
  21. package/dist/proxy/index.d.ts +5 -0
  22. package/dist/proxy/index.d.ts.map +1 -0
  23. package/dist/proxy/index.js +5 -0
  24. package/dist/proxy/index.js.map +1 -0
  25. package/dist/proxy/types.d.ts +54 -0
  26. package/dist/proxy/types.d.ts.map +1 -0
  27. package/dist/proxy/types.js +7 -0
  28. package/dist/proxy/types.js.map +1 -0
  29. package/dist/schemas/blobs.d.ts +76 -0
  30. package/dist/schemas/blobs.d.ts.map +1 -0
  31. package/dist/schemas/blobs.js +63 -0
  32. package/dist/schemas/blobs.js.map +1 -0
  33. package/dist/schemas/common.d.ts +28 -0
  34. package/dist/schemas/common.d.ts.map +1 -0
  35. package/dist/schemas/common.js +26 -0
  36. package/dist/schemas/common.js.map +1 -0
  37. package/dist/schemas/index.d.ts +7 -0
  38. package/dist/schemas/index.d.ts.map +1 -0
  39. package/dist/schemas/index.js +7 -0
  40. package/dist/schemas/index.js.map +1 -0
  41. package/dist/schemas/sync.d.ts +391 -0
  42. package/dist/schemas/sync.d.ts.map +1 -0
  43. package/dist/schemas/sync.js +157 -0
  44. package/dist/schemas/sync.js.map +1 -0
  45. package/dist/scopes/index.d.ts +65 -0
  46. package/dist/scopes/index.d.ts.map +1 -0
  47. package/dist/scopes/index.js +67 -0
  48. package/dist/scopes/index.js.map +1 -0
  49. package/dist/snapshot-chunks.d.ts +26 -0
  50. package/dist/snapshot-chunks.d.ts.map +1 -0
  51. package/dist/snapshot-chunks.js +89 -0
  52. package/dist/snapshot-chunks.js.map +1 -0
  53. package/dist/telemetry.d.ts +114 -0
  54. package/dist/telemetry.d.ts.map +1 -0
  55. package/dist/telemetry.js +113 -0
  56. package/dist/telemetry.js.map +1 -0
  57. package/dist/transforms.d.ts +146 -0
  58. package/dist/transforms.d.ts.map +1 -0
  59. package/dist/transforms.js +155 -0
  60. package/dist/transforms.js.map +1 -0
  61. package/dist/types.d.ts +129 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +20 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/utils/id.d.ts +2 -0
  66. package/dist/utils/id.d.ts.map +1 -0
  67. package/dist/utils/id.js +8 -0
  68. package/dist/utils/id.js.map +1 -0
  69. package/dist/utils/index.d.ts +3 -0
  70. package/dist/utils/index.d.ts.map +1 -0
  71. package/dist/utils/index.js +3 -0
  72. package/dist/utils/index.js.map +1 -0
  73. package/dist/utils/object.d.ts +2 -0
  74. package/dist/utils/object.d.ts.map +1 -0
  75. package/dist/utils/object.js +4 -0
  76. package/dist/utils/object.js.map +1 -0
  77. package/package.json +57 -0
  78. package/src/__tests__/conflict.test.ts +325 -0
  79. package/src/__tests__/telemetry.test.ts +170 -0
  80. package/src/__tests__/utils.test.ts +27 -0
  81. package/src/blobs.ts +202 -0
  82. package/src/conflict.ts +92 -0
  83. package/src/index.ts +36 -0
  84. package/src/kysely-serialize.ts +214 -0
  85. package/src/logger.ts +38 -0
  86. package/src/proxy/index.ts +10 -0
  87. package/src/proxy/types.ts +57 -0
  88. package/src/schemas/blobs.ts +101 -0
  89. package/src/schemas/common.ts +45 -0
  90. package/src/schemas/index.ts +7 -0
  91. package/src/schemas/sync.ts +226 -0
  92. package/src/scopes/index.ts +122 -0
  93. package/src/snapshot-chunks.ts +112 -0
  94. package/src/telemetry.ts +238 -0
  95. package/src/transforms.ts +256 -0
  96. package/src/types.ts +158 -0
  97. package/src/utils/id.ts +7 -0
  98. package/src/utils/index.ts +2 -0
  99. package/src/utils/object.ts +3 -0
package/src/blobs.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @syncular/core - Blob types for media/binary handling
3
+ *
4
+ * Content-addressable blob storage with presigned URL support.
5
+ * Protocol types (BlobRef, BlobMetadata, etc.) live in ./schemas/blobs.ts
6
+ */
7
+
8
+ import type { BlobRef } from './schemas/blobs';
9
+
10
+ // ============================================================================
11
+ // Client Transport Types
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Transport interface for client-server blob communication.
16
+ * This is used by the client blob manager to communicate with the server.
17
+ */
18
+ export interface BlobTransport {
19
+ /**
20
+ * Initiate a blob upload.
21
+ * Returns presigned URL info or indicates blob already exists (dedup).
22
+ */
23
+ initiateUpload(args: {
24
+ hash: string;
25
+ size: number;
26
+ mimeType: string;
27
+ }): Promise<{
28
+ exists: boolean;
29
+ uploadUrl?: string;
30
+ uploadMethod?: 'PUT' | 'POST';
31
+ uploadHeaders?: Record<string, string>;
32
+ }>;
33
+
34
+ /**
35
+ * Complete a blob upload.
36
+ * Call this after uploading to the presigned URL.
37
+ */
38
+ completeUpload(hash: string): Promise<{ ok: boolean; error?: string }>;
39
+
40
+ /**
41
+ * Get a presigned download URL.
42
+ */
43
+ getDownloadUrl(hash: string): Promise<{
44
+ url: string;
45
+ expiresAt: string;
46
+ }>;
47
+ }
48
+
49
+ // ============================================================================
50
+ // Storage Adapter Types (Server-side)
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Options for signing an upload URL.
55
+ */
56
+ export interface BlobSignUploadOptions {
57
+ /** SHA-256 hash (for naming and checksum validation) */
58
+ hash: string;
59
+ /** Content size in bytes */
60
+ size: number;
61
+ /** MIME type */
62
+ mimeType: string;
63
+ /** URL expiration in seconds */
64
+ expiresIn: number;
65
+ }
66
+
67
+ /**
68
+ * Result of signing an upload URL.
69
+ */
70
+ export interface BlobSignedUpload {
71
+ /** The URL to upload to */
72
+ url: string;
73
+ /** HTTP method */
74
+ method: 'PUT' | 'POST';
75
+ /** Required headers */
76
+ headers?: Record<string, string>;
77
+ }
78
+
79
+ /**
80
+ * Options for signing a download URL.
81
+ */
82
+ export interface BlobSignDownloadOptions {
83
+ /** SHA-256 hash */
84
+ hash: string;
85
+ /** URL expiration in seconds */
86
+ expiresIn: number;
87
+ }
88
+
89
+ /**
90
+ * Adapter for blob storage backends (S3, R2, custom).
91
+ * Implementations handle actual storage; the sync server orchestrates.
92
+ */
93
+ export interface BlobStorageAdapter {
94
+ /** Adapter name for logging/debugging */
95
+ readonly name: string;
96
+
97
+ /**
98
+ * Generate a presigned URL for uploading a blob.
99
+ * The URL should enforce checksum validation if the backend supports it.
100
+ */
101
+ signUpload(options: BlobSignUploadOptions): Promise<BlobSignedUpload>;
102
+
103
+ /**
104
+ * Generate a presigned URL for downloading a blob.
105
+ */
106
+ signDownload(options: BlobSignDownloadOptions): Promise<string>;
107
+
108
+ /**
109
+ * Check if a blob exists in storage.
110
+ */
111
+ exists(hash: string): Promise<boolean>;
112
+
113
+ /**
114
+ * Delete a blob (for garbage collection).
115
+ */
116
+ delete(hash: string): Promise<void>;
117
+
118
+ /**
119
+ * Get blob metadata from storage (optional).
120
+ * Used to verify uploads completed successfully.
121
+ */
122
+ getMetadata?(
123
+ hash: string
124
+ ): Promise<{ size: number; mimeType?: string } | null>;
125
+
126
+ /**
127
+ * Store blob data directly (for adapters that support direct storage).
128
+ * Used for snapshot chunks and other internal data.
129
+ */
130
+ put?(
131
+ hash: string,
132
+ data: Uint8Array,
133
+ metadata?: Record<string, unknown>
134
+ ): Promise<void>;
135
+
136
+ /**
137
+ * Store blob data directly from a stream.
138
+ * Preferred for large payloads to avoid full buffering in memory.
139
+ */
140
+ putStream?(
141
+ hash: string,
142
+ stream: ReadableStream<Uint8Array>,
143
+ metadata?: Record<string, unknown>
144
+ ): Promise<void>;
145
+
146
+ /**
147
+ * Get blob data directly (for adapters that support direct retrieval).
148
+ */
149
+ get?(hash: string): Promise<Uint8Array | null>;
150
+
151
+ /**
152
+ * Get blob data directly as a stream (for adapters that support stream retrieval).
153
+ */
154
+ getStream?(hash: string): Promise<ReadableStream<Uint8Array> | null>;
155
+ }
156
+
157
+ // ============================================================================
158
+ // Utility Functions
159
+ // ============================================================================
160
+
161
+ /**
162
+ * Create a BlobRef from upload metadata.
163
+ */
164
+ export function createBlobRef(args: {
165
+ hash: string;
166
+ size: number;
167
+ mimeType: string;
168
+ encrypted?: boolean;
169
+ keyId?: string;
170
+ }): BlobRef {
171
+ const ref: BlobRef = {
172
+ hash: args.hash,
173
+ size: args.size,
174
+ mimeType: args.mimeType,
175
+ };
176
+ if (args.encrypted) {
177
+ ref.encrypted = true;
178
+ if (args.keyId) {
179
+ ref.keyId = args.keyId;
180
+ }
181
+ }
182
+ return ref;
183
+ }
184
+
185
+ /**
186
+ * Parse a blob hash, validating format.
187
+ * @returns The hex hash without prefix, or null if invalid.
188
+ */
189
+ export function parseBlobHash(hash: string): string | null {
190
+ if (!hash.startsWith('sha256:')) return null;
191
+ const hex = hash.slice(7);
192
+ if (hex.length !== 64) return null;
193
+ if (!/^[0-9a-f]+$/i.test(hex)) return null;
194
+ return hex.toLowerCase();
195
+ }
196
+
197
+ /**
198
+ * Create a blob hash string from hex.
199
+ */
200
+ export function createBlobHash(hexHash: string): string {
201
+ return `sha256:${hexHash.toLowerCase()}`;
202
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @syncular/core - Pure conflict detection and field-level merge utilities
3
+ *
4
+ * These are pure functions with no database dependencies.
5
+ * Database-specific conflict detection (triggers, etc.) lives in @syncular/server.
6
+ */
7
+
8
+ import type { MergeResult } from './types';
9
+
10
+ /**
11
+ * Performs field-level merge between client changes and server state.
12
+ *
13
+ * Merge logic:
14
+ * - If only client changed a field -> use client's value
15
+ * - If only server changed a field -> keep server's value
16
+ * - If both changed same field to different values -> true conflict
17
+ *
18
+ * @param baseRow - The row state when client started editing (from base_version)
19
+ * @param serverRow - Current server row state
20
+ * @param clientPayload - Client's intended changes
21
+ * @returns MergeResult indicating if merge is possible and the result
22
+ */
23
+ export function performFieldLevelMerge(
24
+ baseRow: Record<string, unknown> | null,
25
+ serverRow: Record<string, unknown>,
26
+ clientPayload: Record<string, unknown>
27
+ ): MergeResult {
28
+ // If no base row (new insert), client payload wins entirely
29
+ if (!baseRow) {
30
+ return { canMerge: true, mergedPayload: clientPayload };
31
+ }
32
+
33
+ const conflictingFields: string[] = [];
34
+ const mergedPayload: Record<string, unknown> = { ...serverRow };
35
+
36
+ // Check each field in the client payload
37
+ for (const [field, clientValue] of Object.entries(clientPayload)) {
38
+ const baseValue = baseRow[field];
39
+ const serverValue = serverRow[field];
40
+
41
+ const clientChanged = !deepEqual(baseValue, clientValue);
42
+ const serverChanged = !deepEqual(baseValue, serverValue);
43
+
44
+ if (clientChanged && serverChanged) {
45
+ // Both changed the same field
46
+ if (!deepEqual(clientValue, serverValue)) {
47
+ // Changed to different values - true conflict
48
+ conflictingFields.push(field);
49
+ }
50
+ // If they changed to the same value, no conflict - use either
51
+ mergedPayload[field] = clientValue;
52
+ } else if (clientChanged) {
53
+ // Only client changed - use client's value
54
+ mergedPayload[field] = clientValue;
55
+ }
56
+ // If only server changed or neither changed, keep server value (already in mergedPayload)
57
+ }
58
+
59
+ if (conflictingFields.length > 0) {
60
+ return { canMerge: false, conflictingFields };
61
+ }
62
+
63
+ return { canMerge: true, mergedPayload };
64
+ }
65
+
66
+ /**
67
+ * Deep equality check for values (handles primitives, arrays, objects)
68
+ */
69
+ function deepEqual(a: unknown, b: unknown): boolean {
70
+ if (a === b) return true;
71
+ if (a === null || b === null) return a === b;
72
+ if (typeof a !== typeof b) return false;
73
+
74
+ if (typeof a === 'object') {
75
+ if (Array.isArray(a) && Array.isArray(b)) {
76
+ if (a.length !== b.length) return false;
77
+ return a.every((item, index) => deepEqual(item, b[index]));
78
+ }
79
+
80
+ if (Array.isArray(a) || Array.isArray(b)) return false;
81
+
82
+ const aObj = a as Record<string, unknown>;
83
+ const bObj = b as Record<string, unknown>;
84
+ const aKeys = Object.keys(aObj);
85
+ const bKeys = Object.keys(bObj);
86
+
87
+ if (aKeys.length !== bKeys.length) return false;
88
+ return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
89
+ }
90
+
91
+ return false;
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @syncular/core - Shared types and utilities for sync infrastructure
3
+ *
4
+ * This package contains:
5
+ * - Protocol types (commit-log + subscriptions)
6
+ * - Pure conflict detection and merge utilities
7
+ * - Logging utilities
8
+ * - Data transformation hooks (optional)
9
+ * - Blob types for media/binary handling
10
+ * - Zod schemas for runtime validation and OpenAPI
11
+ */
12
+
13
+ // Blob transport/storage types and utilities (protocol types come from ./schemas)
14
+ export * from './blobs';
15
+ // Conflict detection utilities
16
+ export * from './conflict';
17
+ // Kysely plugin utilities
18
+ export * from './kysely-serialize';
19
+ // Logging utilities
20
+ export * from './logger';
21
+ // Proxy protocol types
22
+ export * from './proxy';
23
+ // Schemas (Zod)
24
+ export * from './schemas';
25
+ // Scope types, patterns, and utilities
26
+ export * from './scopes';
27
+ // Snapshot chunk encoding helpers
28
+ export * from './snapshot-chunks';
29
+ // Telemetry abstraction
30
+ export * from './telemetry';
31
+ // Data transformation hooks
32
+ export * from './transforms';
33
+ // Transport and conflict types (protocol types come from ./schemas)
34
+ export * from './types';
35
+ // Shared runtime utilities
36
+ export * from './utils';
@@ -0,0 +1,214 @@
1
+ import {
2
+ type ColumnUpdateNode,
3
+ type KyselyPlugin,
4
+ OperationNodeTransformer,
5
+ type PluginTransformQueryArgs,
6
+ type PluginTransformResultArgs,
7
+ type PrimitiveValueListNode,
8
+ type QueryResult,
9
+ type RootOperationNode,
10
+ type UnknownRow,
11
+ type ValueNode,
12
+ } from 'kysely';
13
+
14
+ type Serializer = (parameter: unknown) => unknown;
15
+ type Deserializer = (parameter: unknown) => unknown;
16
+
17
+ const dateRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
18
+
19
+ function isBufferLike(value: object): value is { buffer: unknown } {
20
+ return 'buffer' in value;
21
+ }
22
+
23
+ function skipTransform(parameter: unknown): boolean {
24
+ if (
25
+ parameter === undefined ||
26
+ parameter === null ||
27
+ typeof parameter === 'bigint' ||
28
+ typeof parameter === 'number'
29
+ ) {
30
+ return true;
31
+ }
32
+
33
+ if (typeof parameter === 'object') {
34
+ return isBufferLike(parameter);
35
+ }
36
+
37
+ return false;
38
+ }
39
+
40
+ function maybeJson(parameter: string): boolean {
41
+ return (
42
+ (parameter.startsWith('{') && parameter.endsWith('}')) ||
43
+ (parameter.startsWith('[') && parameter.endsWith(']'))
44
+ );
45
+ }
46
+
47
+ const defaultSerializer: Serializer = (parameter) => {
48
+ if (skipTransform(parameter) || typeof parameter === 'string') {
49
+ return parameter;
50
+ }
51
+
52
+ if (typeof parameter === 'boolean') {
53
+ return String(parameter);
54
+ }
55
+
56
+ if (parameter instanceof Date) {
57
+ return parameter.toISOString();
58
+ }
59
+
60
+ try {
61
+ return JSON.stringify(parameter);
62
+ } catch {
63
+ return parameter;
64
+ }
65
+ };
66
+
67
+ const defaultDeserializer: Deserializer = (parameter) => {
68
+ if (skipTransform(parameter)) {
69
+ return parameter;
70
+ }
71
+
72
+ if (typeof parameter !== 'string') {
73
+ return parameter;
74
+ }
75
+
76
+ if (parameter === 'true') return true;
77
+ if (parameter === 'false') return false;
78
+ if (dateRegex.test(parameter)) return new Date(parameter);
79
+
80
+ if (maybeJson(parameter)) {
81
+ try {
82
+ return JSON.parse(parameter);
83
+ } catch {
84
+ return parameter;
85
+ }
86
+ }
87
+
88
+ return parameter;
89
+ };
90
+
91
+ class SerializeParametersTransformer extends OperationNodeTransformer {
92
+ readonly #serializer: Serializer;
93
+
94
+ constructor(serializer: Serializer) {
95
+ super();
96
+ this.#serializer = serializer;
97
+ }
98
+
99
+ protected override transformPrimitiveValueList(
100
+ node: PrimitiveValueListNode
101
+ ): PrimitiveValueListNode {
102
+ return {
103
+ ...node,
104
+ values: node.values.map((v) => this.#serializer(v)),
105
+ };
106
+ }
107
+
108
+ protected override transformColumnUpdate(
109
+ node: ColumnUpdateNode,
110
+ queryId?: { readonly queryId: string }
111
+ ): ColumnUpdateNode {
112
+ const valueNode = node.value;
113
+ if (valueNode.kind !== 'ValueNode') {
114
+ return super.transformColumnUpdate(node, queryId);
115
+ }
116
+
117
+ const currentValue = (valueNode as ValueNode).value;
118
+ const serializedValue = this.#serializer(currentValue);
119
+ if (currentValue === serializedValue) {
120
+ return super.transformColumnUpdate(node, queryId);
121
+ }
122
+
123
+ const updatedValue: ValueNode = {
124
+ ...(valueNode as ValueNode),
125
+ value: serializedValue,
126
+ };
127
+
128
+ return super.transformColumnUpdate(
129
+ { ...node, value: updatedValue },
130
+ queryId
131
+ );
132
+ }
133
+
134
+ protected override transformValue(node: ValueNode): ValueNode {
135
+ return { ...node, value: this.#serializer(node.value) };
136
+ }
137
+ }
138
+
139
+ class BaseSerializePlugin implements KyselyPlugin {
140
+ readonly #transformer: SerializeParametersTransformer;
141
+ readonly #deserializer: Deserializer;
142
+ readonly #skipNodeSet: Set<RootOperationNode['kind']> | null;
143
+ readonly #ctx: WeakSet<object> | null;
144
+
145
+ /**
146
+ * Base class for {@link SerializePlugin}, without default options.
147
+ */
148
+ constructor(
149
+ serializer: Serializer,
150
+ deserializer: Deserializer,
151
+ skipNodeKind: Array<RootOperationNode['kind']>
152
+ ) {
153
+ this.#transformer = new SerializeParametersTransformer(serializer);
154
+ this.#deserializer = deserializer;
155
+ if (skipNodeKind.length > 0) {
156
+ this.#skipNodeSet = new Set(skipNodeKind);
157
+ this.#ctx = new WeakSet<object>();
158
+ } else {
159
+ this.#skipNodeSet = null;
160
+ this.#ctx = null;
161
+ }
162
+ }
163
+
164
+ transformQuery({
165
+ node,
166
+ queryId,
167
+ }: PluginTransformQueryArgs): RootOperationNode {
168
+ if (this.#skipNodeSet?.has(node.kind)) {
169
+ this.#ctx?.add(queryId);
170
+ return node;
171
+ }
172
+ return this.#transformer.transformNode(node);
173
+ }
174
+
175
+ async transformResult({
176
+ result,
177
+ queryId,
178
+ }: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
179
+ if (this.#ctx?.has(queryId)) {
180
+ return result;
181
+ }
182
+ return { ...result, rows: this.#parseRows(result.rows) };
183
+ }
184
+
185
+ #parseRows(rows: UnknownRow[]): UnknownRow[] {
186
+ const out: UnknownRow[] = [];
187
+ for (const row of rows) {
188
+ if (!row) continue;
189
+ const parsed: Record<string, unknown> = {};
190
+ for (const [key, value] of Object.entries(row)) {
191
+ parsed[key] = this.#deserializer(value);
192
+ }
193
+ out.push(parsed);
194
+ }
195
+ return out;
196
+ }
197
+ }
198
+
199
+ interface SerializePluginOptions {
200
+ serializer?: Serializer;
201
+ deserializer?: Deserializer;
202
+ skipNodeKind?: Array<RootOperationNode['kind']>;
203
+ }
204
+
205
+ export class SerializePlugin extends BaseSerializePlugin {
206
+ constructor(options: SerializePluginOptions = {}) {
207
+ const {
208
+ serializer = defaultSerializer,
209
+ deserializer = defaultDeserializer,
210
+ skipNodeKind = [],
211
+ } = options;
212
+ super(serializer, deserializer, skipNodeKind);
213
+ }
214
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @syncular/core - Structured logging utilities for sync operations
3
+ *
4
+ * Uses the active telemetry backend configured via `configureSyncTelemetry()`.
5
+ */
6
+
7
+ import { getSyncTelemetry, type SyncTelemetryEvent } from './telemetry';
8
+
9
+ /**
10
+ * Sync log event structure.
11
+ */
12
+ export type SyncLogEvent = SyncTelemetryEvent;
13
+
14
+ /**
15
+ * Logger function type.
16
+ */
17
+ export type SyncLogger = (event: SyncLogEvent) => void;
18
+
19
+ /**
20
+ * Log a sync event using the currently configured telemetry backend.
21
+ */
22
+ export const logSyncEvent: SyncLogger = (event) => {
23
+ getSyncTelemetry().log(event);
24
+ };
25
+
26
+ /**
27
+ * Create a timer for measuring operation duration.
28
+ * Returns the elapsed time in milliseconds when called.
29
+ *
30
+ * @example
31
+ * const elapsed = createSyncTimer();
32
+ * await doSomeWork();
33
+ * logSyncEvent({ event: 'work_complete', durationMs: elapsed() });
34
+ */
35
+ export function createSyncTimer(): () => number {
36
+ const start = performance.now();
37
+ return () => Math.round(performance.now() - start);
38
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @syncular/core - Proxy Protocol Exports
3
+ */
4
+
5
+ export type {
6
+ ProxyHandshake,
7
+ ProxyHandshakeAck,
8
+ ProxyMessage,
9
+ ProxyResponse,
10
+ } from './types';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @syncular/core - Proxy Protocol Types
3
+ *
4
+ * Shared protocol types between proxy client (Kysely dialect) and server (WebSocket handler).
5
+ */
6
+
7
+ /**
8
+ * Message sent from proxy client to server.
9
+ */
10
+ export interface ProxyMessage {
11
+ /** Correlation ID for matching request/response */
12
+ id: string;
13
+ /** Message type */
14
+ type: 'query' | 'begin' | 'commit' | 'rollback';
15
+ /** SQL query (for 'query' type) */
16
+ sql?: string;
17
+ /** Query parameters (for 'query' type) */
18
+ parameters?: readonly unknown[];
19
+ }
20
+
21
+ /**
22
+ * Response sent from server to proxy client.
23
+ */
24
+ export interface ProxyResponse {
25
+ /** Correlation ID matching the request */
26
+ id: string;
27
+ /** Response type */
28
+ type: 'result' | 'error';
29
+ /** Query result rows (for SELECT queries) */
30
+ rows?: unknown[];
31
+ /** Number of affected rows (for mutations) */
32
+ rowCount?: number;
33
+ /** Error message (for 'error' type) */
34
+ error?: string;
35
+ }
36
+
37
+ /**
38
+ * Handshake message sent when connection is established.
39
+ */
40
+ export interface ProxyHandshake {
41
+ type: 'handshake';
42
+ /** Actor ID for oplog tracking */
43
+ actorId: string;
44
+ /** Client ID for oplog tracking */
45
+ clientId: string;
46
+ }
47
+
48
+ /**
49
+ * Handshake acknowledgement from server.
50
+ */
51
+ export interface ProxyHandshakeAck {
52
+ type: 'handshake_ack';
53
+ /** Whether handshake was successful */
54
+ ok: boolean;
55
+ /** Error message if handshake failed */
56
+ error?: string;
57
+ }