@syncular/core 0.0.1 → 0.0.2-127
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 +24 -0
- package/dist/blobs.d.ts +9 -4
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +0 -12
- package/dist/blobs.js.map +1 -1
- package/dist/column-codecs.d.ts +55 -0
- package/dist/column-codecs.d.ts.map +1 -0
- package/dist/column-codecs.js +124 -0
- package/dist/column-codecs.js.map +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -7
- package/dist/index.js.map +1 -1
- package/dist/kysely-serialize.d.ts +1 -1
- package/dist/kysely-serialize.d.ts.map +1 -1
- package/dist/logger.d.ts +7 -32
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +6 -40
- package/dist/logger.js.map +1 -1
- package/dist/proxy/types.d.ts +0 -9
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -3
- package/dist/schemas/sync.d.ts +120 -6
- package/dist/schemas/sync.d.ts.map +1 -1
- package/dist/schemas/sync.js +17 -3
- package/dist/schemas/sync.js.map +1 -1
- package/dist/scopes/index.d.ts +39 -64
- package/dist/scopes/index.d.ts.map +1 -1
- package/dist/scopes/index.js +9 -154
- package/dist/scopes/index.js.map +1 -1
- package/dist/snapshot-chunks.d.ts +26 -0
- package/dist/snapshot-chunks.d.ts.map +1 -0
- package/dist/snapshot-chunks.js +89 -0
- package/dist/snapshot-chunks.js.map +1 -0
- package/dist/telemetry.d.ts +114 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +113 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +12 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +8 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/object.d.ts +2 -0
- package/dist/utils/object.d.ts.map +1 -0
- package/dist/utils/object.js +4 -0
- package/dist/utils/object.js.map +1 -0
- package/package.json +28 -8
- package/src/__tests__/telemetry.test.ts +170 -0
- package/src/__tests__/utils.test.ts +27 -0
- package/src/blobs.ts +15 -14
- package/src/column-codecs.ts +228 -0
- package/src/index.ts +15 -41
- package/src/kysely-serialize.ts +1 -1
- package/src/logger.ts +10 -68
- package/src/proxy/types.ts +0 -10
- package/src/schemas/sync.ts +27 -3
- package/src/scopes/index.ts +72 -200
- package/src/snapshot-chunks.ts +112 -0
- package/src/telemetry.ts +238 -0
- package/src/types.ts +14 -18
- package/src/utils/id.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/object.ts +3 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { logSyncEvent } from '../logger';
|
|
3
|
+
import {
|
|
4
|
+
captureSyncException,
|
|
5
|
+
configureSyncTelemetry,
|
|
6
|
+
countSyncMetric,
|
|
7
|
+
distributionSyncMetric,
|
|
8
|
+
gaugeSyncMetric,
|
|
9
|
+
getSyncTelemetry,
|
|
10
|
+
resetSyncTelemetry,
|
|
11
|
+
type SyncMetricOptions,
|
|
12
|
+
type SyncSpan,
|
|
13
|
+
type SyncSpanOptions,
|
|
14
|
+
type SyncTelemetry,
|
|
15
|
+
type SyncTelemetryEvent,
|
|
16
|
+
startSyncSpan,
|
|
17
|
+
} from '../telemetry';
|
|
18
|
+
|
|
19
|
+
interface CapturedCountMetric {
|
|
20
|
+
name: string;
|
|
21
|
+
value: number | undefined;
|
|
22
|
+
options: SyncMetricOptions | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CapturedValueMetric {
|
|
26
|
+
name: string;
|
|
27
|
+
value: number;
|
|
28
|
+
options: SyncMetricOptions | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createTestTelemetry(calls: {
|
|
32
|
+
logs: SyncTelemetryEvent[];
|
|
33
|
+
countMetrics: CapturedCountMetric[];
|
|
34
|
+
gaugeMetrics: CapturedValueMetric[];
|
|
35
|
+
distributionMetrics: CapturedValueMetric[];
|
|
36
|
+
spans: SyncSpanOptions[];
|
|
37
|
+
exceptions: Array<{
|
|
38
|
+
error: unknown;
|
|
39
|
+
context: Record<string, unknown> | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
}): SyncTelemetry {
|
|
42
|
+
return {
|
|
43
|
+
log(event) {
|
|
44
|
+
calls.logs.push(event);
|
|
45
|
+
},
|
|
46
|
+
tracer: {
|
|
47
|
+
startSpan(options, callback) {
|
|
48
|
+
calls.spans.push(options);
|
|
49
|
+
const span: SyncSpan = {
|
|
50
|
+
setAttribute() {},
|
|
51
|
+
setAttributes() {},
|
|
52
|
+
setStatus() {},
|
|
53
|
+
};
|
|
54
|
+
return callback(span);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
metrics: {
|
|
58
|
+
count(name, value, options) {
|
|
59
|
+
calls.countMetrics.push({ name, value, options });
|
|
60
|
+
},
|
|
61
|
+
gauge(name, value, options) {
|
|
62
|
+
calls.gaugeMetrics.push({ name, value, options });
|
|
63
|
+
},
|
|
64
|
+
distribution(name, value, options) {
|
|
65
|
+
calls.distributionMetrics.push({ name, value, options });
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
captureException(error, context) {
|
|
69
|
+
calls.exceptions.push({ error, context });
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('sync telemetry configuration', () => {
|
|
75
|
+
test('routes logger, metrics, spans, and exceptions to configured backend', () => {
|
|
76
|
+
const calls = {
|
|
77
|
+
logs: [] as SyncTelemetryEvent[],
|
|
78
|
+
countMetrics: [] as CapturedCountMetric[],
|
|
79
|
+
gaugeMetrics: [] as CapturedValueMetric[],
|
|
80
|
+
distributionMetrics: [] as CapturedValueMetric[],
|
|
81
|
+
spans: [] as SyncSpanOptions[],
|
|
82
|
+
exceptions: [] as Array<{
|
|
83
|
+
error: unknown;
|
|
84
|
+
context: Record<string, unknown> | undefined;
|
|
85
|
+
}>,
|
|
86
|
+
};
|
|
87
|
+
const telemetry = createTestTelemetry(calls);
|
|
88
|
+
const previous = getSyncTelemetry();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
configureSyncTelemetry(telemetry);
|
|
92
|
+
|
|
93
|
+
logSyncEvent({ event: 'sync.test.log', rowCount: 3 });
|
|
94
|
+
|
|
95
|
+
const spanResult = startSyncSpan(
|
|
96
|
+
{
|
|
97
|
+
name: 'sync.test.span',
|
|
98
|
+
op: 'sync.test',
|
|
99
|
+
attributes: { transport: 'ws' },
|
|
100
|
+
},
|
|
101
|
+
() => 'done'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
countSyncMetric('sync.test.count', 2, {
|
|
105
|
+
attributes: { source: 'unit-test' },
|
|
106
|
+
});
|
|
107
|
+
gaugeSyncMetric('sync.test.gauge', 7, { unit: 'millisecond' });
|
|
108
|
+
distributionSyncMetric('sync.test.dist', 13);
|
|
109
|
+
captureSyncException(new Error('boom'), {
|
|
110
|
+
operation: 'unit-test',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(spanResult).toBe('done');
|
|
114
|
+
expect(calls.logs).toEqual([{ event: 'sync.test.log', rowCount: 3 }]);
|
|
115
|
+
expect(calls.spans).toEqual([
|
|
116
|
+
{
|
|
117
|
+
name: 'sync.test.span',
|
|
118
|
+
op: 'sync.test',
|
|
119
|
+
attributes: { transport: 'ws' },
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
expect(calls.countMetrics).toEqual([
|
|
123
|
+
{
|
|
124
|
+
name: 'sync.test.count',
|
|
125
|
+
value: 2,
|
|
126
|
+
options: { attributes: { source: 'unit-test' } },
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
expect(calls.gaugeMetrics).toEqual([
|
|
130
|
+
{
|
|
131
|
+
name: 'sync.test.gauge',
|
|
132
|
+
value: 7,
|
|
133
|
+
options: { unit: 'millisecond' },
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
expect(calls.distributionMetrics).toEqual([
|
|
137
|
+
{
|
|
138
|
+
name: 'sync.test.dist',
|
|
139
|
+
value: 13,
|
|
140
|
+
options: undefined,
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
expect(calls.exceptions).toHaveLength(1);
|
|
144
|
+
expect(calls.exceptions[0]?.context).toEqual({ operation: 'unit-test' });
|
|
145
|
+
} finally {
|
|
146
|
+
configureSyncTelemetry(previous);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('resetSyncTelemetry swaps out custom telemetry backend', () => {
|
|
151
|
+
const calls = {
|
|
152
|
+
logs: [] as SyncTelemetryEvent[],
|
|
153
|
+
countMetrics: [] as CapturedCountMetric[],
|
|
154
|
+
gaugeMetrics: [] as CapturedValueMetric[],
|
|
155
|
+
distributionMetrics: [] as CapturedValueMetric[],
|
|
156
|
+
spans: [] as SyncSpanOptions[],
|
|
157
|
+
exceptions: [] as Array<{
|
|
158
|
+
error: unknown;
|
|
159
|
+
context: Record<string, unknown> | undefined;
|
|
160
|
+
}>,
|
|
161
|
+
};
|
|
162
|
+
const telemetry = createTestTelemetry(calls);
|
|
163
|
+
|
|
164
|
+
configureSyncTelemetry(telemetry);
|
|
165
|
+
resetSyncTelemetry();
|
|
166
|
+
logSyncEvent({ event: 'sync.default.logger' });
|
|
167
|
+
|
|
168
|
+
expect(calls.logs).toHaveLength(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { isRecord, randomId } from '../utils';
|
|
3
|
+
|
|
4
|
+
describe('isRecord', () => {
|
|
5
|
+
it('returns true for plain objects', () => {
|
|
6
|
+
expect(isRecord({ a: 1 })).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns false for null and arrays', () => {
|
|
10
|
+
expect(isRecord(null)).toBe(false);
|
|
11
|
+
expect(isRecord([])).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('randomId', () => {
|
|
16
|
+
it('returns a non-empty string id', () => {
|
|
17
|
+
const id = randomId();
|
|
18
|
+
expect(typeof id).toBe('string');
|
|
19
|
+
expect(id.length > 0).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('generates different ids across sequential calls', () => {
|
|
23
|
+
const first = randomId();
|
|
24
|
+
const second = randomId();
|
|
25
|
+
expect(first).not.toBe(second);
|
|
26
|
+
});
|
|
27
|
+
});
|
package/src/blobs.ts
CHANGED
|
@@ -133,10 +133,25 @@ export interface BlobStorageAdapter {
|
|
|
133
133
|
metadata?: Record<string, unknown>
|
|
134
134
|
): Promise<void>;
|
|
135
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
|
+
|
|
136
146
|
/**
|
|
137
147
|
* Get blob data directly (for adapters that support direct retrieval).
|
|
138
148
|
*/
|
|
139
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>;
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
// ============================================================================
|
|
@@ -185,17 +200,3 @@ export function parseBlobHash(hash: string): string | null {
|
|
|
185
200
|
export function createBlobHash(hexHash: string): string {
|
|
186
201
|
return `sha256:${hexHash.toLowerCase()}`;
|
|
187
202
|
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Check if a value looks like a BlobRef.
|
|
191
|
-
*/
|
|
192
|
-
export function isBlobRef(value: unknown): value is BlobRef {
|
|
193
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
194
|
-
const obj = value as Record<string, unknown>;
|
|
195
|
-
return (
|
|
196
|
-
typeof obj.hash === 'string' &&
|
|
197
|
-
obj.hash.startsWith('sha256:') &&
|
|
198
|
-
typeof obj.size === 'number' &&
|
|
199
|
-
typeof obj.mimeType === 'string'
|
|
200
|
-
);
|
|
201
|
-
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export type ColumnCodecDialect = 'sqlite' | 'postgres';
|
|
2
|
+
|
|
3
|
+
export interface ColumnCodecTypeImport {
|
|
4
|
+
name: string;
|
|
5
|
+
from: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ColumnCodecType =
|
|
9
|
+
| string
|
|
10
|
+
| { type: string; import?: ColumnCodecTypeImport };
|
|
11
|
+
|
|
12
|
+
export interface ColumnCodec<App, Db> {
|
|
13
|
+
ts: ColumnCodecType;
|
|
14
|
+
toDb(value: App): Db;
|
|
15
|
+
fromDb(value: Db): App;
|
|
16
|
+
dialects?: Partial<
|
|
17
|
+
Record<
|
|
18
|
+
ColumnCodecDialect,
|
|
19
|
+
{
|
|
20
|
+
toDb?(value: App): Db;
|
|
21
|
+
fromDb?(value: Db): App;
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AnyColumnCodec = ColumnCodec<unknown, unknown>;
|
|
28
|
+
|
|
29
|
+
export interface ColumnCodecColumn {
|
|
30
|
+
table: string;
|
|
31
|
+
column: string;
|
|
32
|
+
sqlType?: string;
|
|
33
|
+
nullable?: boolean;
|
|
34
|
+
isPrimaryKey?: boolean;
|
|
35
|
+
hasDefault?: boolean;
|
|
36
|
+
dialect?: ColumnCodecDialect;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TableColumnCodecs = Record<string, AnyColumnCodec>;
|
|
40
|
+
|
|
41
|
+
export type ColumnCodecSource = (
|
|
42
|
+
column: ColumnCodecColumn
|
|
43
|
+
) => AnyColumnCodec | undefined;
|
|
44
|
+
|
|
45
|
+
function hasCodecs(tableCodecs: TableColumnCodecs): boolean {
|
|
46
|
+
return Object.keys(tableCodecs).length > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveCodecToDb(
|
|
50
|
+
codec: AnyColumnCodec,
|
|
51
|
+
dialect: ColumnCodecDialect
|
|
52
|
+
): (value: unknown) => unknown {
|
|
53
|
+
return codec.dialects?.[dialect]?.toDb ?? codec.toDb;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveCodecFromDb(
|
|
57
|
+
codec: AnyColumnCodec,
|
|
58
|
+
dialect: ColumnCodecDialect
|
|
59
|
+
): (value: unknown) => unknown {
|
|
60
|
+
return codec.dialects?.[dialect]?.fromDb ?? codec.fromDb;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toTableColumnCodecs(
|
|
64
|
+
table: string,
|
|
65
|
+
codecSource: ColumnCodecSource | undefined,
|
|
66
|
+
columns: Iterable<string>,
|
|
67
|
+
options: {
|
|
68
|
+
dialect?: ColumnCodecDialect;
|
|
69
|
+
sqlTypes?: Record<string, string | undefined>;
|
|
70
|
+
} = {}
|
|
71
|
+
): TableColumnCodecs {
|
|
72
|
+
if (!codecSource) return {};
|
|
73
|
+
const out: TableColumnCodecs = {};
|
|
74
|
+
|
|
75
|
+
for (const column of columns) {
|
|
76
|
+
if (column.length === 0) continue;
|
|
77
|
+
const codec = codecSource({
|
|
78
|
+
table,
|
|
79
|
+
column,
|
|
80
|
+
sqlType: options.sqlTypes?.[column],
|
|
81
|
+
dialect: options.dialect,
|
|
82
|
+
});
|
|
83
|
+
if (codec) out[column] = codec;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function applyCodecToDbValue(
|
|
90
|
+
codec: AnyColumnCodec,
|
|
91
|
+
value: unknown,
|
|
92
|
+
dialect: ColumnCodecDialect = 'sqlite'
|
|
93
|
+
): unknown {
|
|
94
|
+
if (value === null || value === undefined) return value;
|
|
95
|
+
const transform = resolveCodecToDb(codec, dialect);
|
|
96
|
+
return transform(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function applyCodecFromDbValue(
|
|
100
|
+
codec: AnyColumnCodec,
|
|
101
|
+
value: unknown,
|
|
102
|
+
dialect: ColumnCodecDialect = 'sqlite'
|
|
103
|
+
): unknown {
|
|
104
|
+
if (value === null || value === undefined) return value;
|
|
105
|
+
const transform = resolveCodecFromDb(codec, dialect);
|
|
106
|
+
return transform(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function applyCodecsToDbRow(
|
|
110
|
+
row: Record<string, unknown>,
|
|
111
|
+
tableCodecs: TableColumnCodecs,
|
|
112
|
+
dialect: ColumnCodecDialect = 'sqlite'
|
|
113
|
+
): Record<string, unknown> {
|
|
114
|
+
if (!hasCodecs(tableCodecs)) return { ...row };
|
|
115
|
+
|
|
116
|
+
const transformed: Record<string, unknown> = { ...row };
|
|
117
|
+
for (const [column, codec] of Object.entries(tableCodecs)) {
|
|
118
|
+
if (!(column in transformed)) continue;
|
|
119
|
+
transformed[column] = applyCodecToDbValue(
|
|
120
|
+
codec,
|
|
121
|
+
transformed[column],
|
|
122
|
+
dialect
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return transformed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function applyCodecsFromDbRow(
|
|
129
|
+
row: Record<string, unknown>,
|
|
130
|
+
tableCodecs: TableColumnCodecs,
|
|
131
|
+
dialect: ColumnCodecDialect = 'sqlite'
|
|
132
|
+
): Record<string, unknown> {
|
|
133
|
+
if (!hasCodecs(tableCodecs)) return { ...row };
|
|
134
|
+
|
|
135
|
+
const transformed: Record<string, unknown> = { ...row };
|
|
136
|
+
for (const [column, codec] of Object.entries(tableCodecs)) {
|
|
137
|
+
if (!(column in transformed)) continue;
|
|
138
|
+
transformed[column] = applyCodecFromDbValue(
|
|
139
|
+
codec,
|
|
140
|
+
transformed[column],
|
|
141
|
+
dialect
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return transformed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseBooleanValue(value: unknown): boolean {
|
|
148
|
+
if (typeof value === 'boolean') return value;
|
|
149
|
+
if (typeof value === 'number') return value === 1;
|
|
150
|
+
if (typeof value === 'string') {
|
|
151
|
+
const normalized = value.trim().toLowerCase();
|
|
152
|
+
return normalized === '1' || normalized === 'true';
|
|
153
|
+
}
|
|
154
|
+
return Boolean(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function numberBoolean(): ColumnCodec<
|
|
158
|
+
boolean,
|
|
159
|
+
number | boolean | string
|
|
160
|
+
> {
|
|
161
|
+
return {
|
|
162
|
+
ts: 'boolean',
|
|
163
|
+
toDb: (value) => (value ? 1 : 0),
|
|
164
|
+
fromDb: (value) => parseBooleanValue(value),
|
|
165
|
+
dialects: {
|
|
166
|
+
postgres: {
|
|
167
|
+
toDb: (value) => value,
|
|
168
|
+
fromDb: (value) => parseBooleanValue(value),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface StringJsonCodecOptions<T> {
|
|
175
|
+
ts?: ColumnCodecType;
|
|
176
|
+
import?: ColumnCodecTypeImport;
|
|
177
|
+
stringify?: (value: T) => string;
|
|
178
|
+
parse?: (value: string) => T;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function stringJson<T = unknown>(
|
|
182
|
+
options: StringJsonCodecOptions<T> = {}
|
|
183
|
+
): ColumnCodec<T, string | T> {
|
|
184
|
+
const stringify = options.stringify ?? ((value: T) => JSON.stringify(value));
|
|
185
|
+
const parse = options.parse ?? ((value: string) => JSON.parse(value) as T);
|
|
186
|
+
|
|
187
|
+
const ts: ColumnCodecType =
|
|
188
|
+
options.ts ??
|
|
189
|
+
(options.import
|
|
190
|
+
? { type: options.import.name, import: options.import }
|
|
191
|
+
: 'unknown');
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
ts,
|
|
195
|
+
toDb: (value) => stringify(value),
|
|
196
|
+
fromDb: (value) => {
|
|
197
|
+
if (typeof value === 'string') {
|
|
198
|
+
return parse(value);
|
|
199
|
+
}
|
|
200
|
+
return value as T;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function timestampDate(): ColumnCodec<Date, string | Date> {
|
|
206
|
+
return {
|
|
207
|
+
ts: 'Date',
|
|
208
|
+
toDb: (value) => value.toISOString(),
|
|
209
|
+
fromDb: (value) =>
|
|
210
|
+
value instanceof Date ? value : new Date(String(value)),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function dateString(): ColumnCodec<string, string | Date> {
|
|
215
|
+
return {
|
|
216
|
+
ts: 'string',
|
|
217
|
+
toDb: (value) => value,
|
|
218
|
+
fromDb: (value) =>
|
|
219
|
+
value instanceof Date ? value.toISOString().slice(0, 10) : String(value),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const codecs = {
|
|
224
|
+
numberBoolean,
|
|
225
|
+
stringJson,
|
|
226
|
+
timestampDate,
|
|
227
|
+
dateString,
|
|
228
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -11,54 +11,28 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// Blob transport/storage types and utilities (protocol types come from ./schemas)
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
BlobSignUploadOptions,
|
|
18
|
-
BlobStorageAdapter,
|
|
19
|
-
BlobTransport,
|
|
20
|
-
} from './blobs';
|
|
21
|
-
export {
|
|
22
|
-
createBlobHash,
|
|
23
|
-
createBlobRef,
|
|
24
|
-
parseBlobHash,
|
|
25
|
-
} from './blobs';
|
|
14
|
+
export * from './blobs';
|
|
15
|
+
// Column-level codecs shared by typegen and runtime paths
|
|
16
|
+
export * from './column-codecs';
|
|
26
17
|
// Conflict detection utilities
|
|
18
|
+
export * from './conflict';
|
|
27
19
|
// Kysely plugin utilities
|
|
28
|
-
export
|
|
20
|
+
export * from './kysely-serialize';
|
|
29
21
|
// Logging utilities
|
|
30
|
-
export
|
|
31
|
-
createSyncTimer,
|
|
32
|
-
logSyncEvent,
|
|
33
|
-
} from './logger';
|
|
22
|
+
export * from './logger';
|
|
34
23
|
// Proxy protocol types
|
|
35
|
-
export
|
|
36
|
-
ProxyHandshake,
|
|
37
|
-
ProxyHandshakeAck,
|
|
38
|
-
ProxyMessage,
|
|
39
|
-
ProxyResponse,
|
|
40
|
-
} from './proxy';
|
|
24
|
+
export * from './proxy';
|
|
41
25
|
// Schemas (Zod)
|
|
42
26
|
export * from './schemas';
|
|
43
27
|
// Scope types, patterns, and utilities
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} from './scopes';
|
|
50
|
-
export { extractScopeVars, normalizeScopes } from './scopes';
|
|
28
|
+
export * from './scopes';
|
|
29
|
+
// Snapshot chunk encoding helpers
|
|
30
|
+
export * from './snapshot-chunks';
|
|
31
|
+
// Telemetry abstraction
|
|
32
|
+
export * from './telemetry';
|
|
51
33
|
// Data transformation hooks
|
|
52
34
|
export * from './transforms';
|
|
53
|
-
|
|
54
35
|
// Transport and conflict types (protocol types come from ./schemas)
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
MergeResultConflict,
|
|
59
|
-
MergeResultOk,
|
|
60
|
-
SyncTransport,
|
|
61
|
-
SyncTransportBlobs,
|
|
62
|
-
SyncTransportOptions,
|
|
63
|
-
} from './types';
|
|
64
|
-
export { SyncTransportError } from './types';
|
|
36
|
+
export * from './types';
|
|
37
|
+
// Shared runtime utilities
|
|
38
|
+
export * from './utils';
|
package/src/kysely-serialize.ts
CHANGED
|
@@ -196,7 +196,7 @@ class BaseSerializePlugin implements KyselyPlugin {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
interface SerializePluginOptions {
|
|
200
200
|
serializer?: Serializer;
|
|
201
201
|
deserializer?: Deserializer;
|
|
202
202
|
skipNodeKind?: Array<RootOperationNode['kind']>;
|
package/src/logger.ts
CHANGED
|
@@ -1,69 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/core - Structured logging utilities for sync operations
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Each log event includes a timestamp and event type.
|
|
4
|
+
* Uses the active telemetry backend configured via `configureSyncTelemetry()`.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
* Sync log event structure
|
|
10
|
-
*/
|
|
11
|
-
interface SyncLogEvent {
|
|
12
|
-
/** Event type identifier */
|
|
13
|
-
event: string;
|
|
14
|
-
/** User ID (optional) */
|
|
15
|
-
userId?: string;
|
|
16
|
-
/** Operation duration in milliseconds (optional) */
|
|
17
|
-
durationMs?: number;
|
|
18
|
-
/** Number of rows affected (optional) */
|
|
19
|
-
rowCount?: number;
|
|
20
|
-
/** Whether a full reset was required (optional) */
|
|
21
|
-
resetRequired?: boolean;
|
|
22
|
-
/** Error message if operation failed (optional) */
|
|
23
|
-
error?: string;
|
|
24
|
-
/** Additional arbitrary properties */
|
|
25
|
-
[key: string]: unknown;
|
|
26
|
-
}
|
|
7
|
+
import { getSyncTelemetry, type SyncTelemetryEvent } from './telemetry';
|
|
27
8
|
|
|
28
9
|
/**
|
|
29
|
-
*
|
|
10
|
+
* Sync log event structure.
|
|
30
11
|
*/
|
|
31
|
-
type
|
|
12
|
+
export type SyncLogEvent = SyncTelemetryEvent;
|
|
32
13
|
|
|
33
14
|
/**
|
|
34
|
-
*
|
|
35
|
-
* Non-blocking - defers logging to avoid blocking the event loop.
|
|
36
|
-
*
|
|
37
|
-
* On server (Node.js), uses setImmediate.
|
|
38
|
-
* On client (browser), uses setTimeout(0).
|
|
15
|
+
* Logger function type.
|
|
39
16
|
*/
|
|
40
|
-
|
|
41
|
-
// Detect environment
|
|
42
|
-
const isNode =
|
|
43
|
-
typeof globalThis !== 'undefined' &&
|
|
44
|
-
typeof globalThis.setImmediate === 'function';
|
|
45
|
-
|
|
46
|
-
const defer = isNode
|
|
47
|
-
? (fn: () => void) => globalThis.setImmediate(fn)
|
|
48
|
-
: (fn: () => void) => setTimeout(fn, 0);
|
|
49
|
-
|
|
50
|
-
return (event: SyncLogEvent) => {
|
|
51
|
-
defer(() => {
|
|
52
|
-
console.log(
|
|
53
|
-
JSON.stringify({
|
|
54
|
-
timestamp: new Date().toISOString(),
|
|
55
|
-
...event,
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
}
|
|
17
|
+
export type SyncLogger = (event: SyncLogEvent) => void;
|
|
61
18
|
|
|
62
19
|
/**
|
|
63
|
-
* Log a sync event using the
|
|
64
|
-
* For custom logging, create your own logger with createDefaultLogger pattern.
|
|
20
|
+
* Log a sync event using the currently configured telemetry backend.
|
|
65
21
|
*/
|
|
66
|
-
export const logSyncEvent: SyncLogger =
|
|
22
|
+
export const logSyncEvent: SyncLogger = (event) => {
|
|
23
|
+
getSyncTelemetry().log(event);
|
|
24
|
+
};
|
|
67
25
|
|
|
68
26
|
/**
|
|
69
27
|
* Create a timer for measuring operation duration.
|
|
@@ -78,19 +36,3 @@ export function createSyncTimer(): () => number {
|
|
|
78
36
|
const start = performance.now();
|
|
79
37
|
return () => Math.round(performance.now() - start);
|
|
80
38
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Create a scoped logger that automatically adds context to all events.
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* const log = createScopedLogger({ userId: 'user123', shape: 'teams' });
|
|
87
|
-
* log({ event: 'pull_start' }); // Includes userId and shape
|
|
88
|
-
*/
|
|
89
|
-
export function createScopedLogger(
|
|
90
|
-
context: Record<string, unknown>,
|
|
91
|
-
baseLogger: SyncLogger = logSyncEvent
|
|
92
|
-
): SyncLogger {
|
|
93
|
-
return (event: SyncLogEvent) => {
|
|
94
|
-
baseLogger({ ...context, ...event });
|
|
95
|
-
};
|
|
96
|
-
}
|
package/src/proxy/types.ts
CHANGED
|
@@ -34,16 +34,6 @@ export interface ProxyResponse {
|
|
|
34
34
|
error?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Options for establishing a proxy connection.
|
|
39
|
-
*/
|
|
40
|
-
export interface ProxyConnectOptions {
|
|
41
|
-
/** Actor ID for oplog tracking */
|
|
42
|
-
actorId: string;
|
|
43
|
-
/** Client ID for oplog tracking */
|
|
44
|
-
clientId: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
/**
|
|
48
38
|
* Handshake message sent when connection is established.
|
|
49
39
|
*/
|