@syncular/core 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 (72) hide show
  1. package/dist/blobs.d.ts +137 -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 +21 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +30 -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 +46 -0
  18. package/dist/logger.d.ts.map +1 -0
  19. package/dist/logger.js +48 -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 +156 -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/transforms.d.ts +146 -0
  50. package/dist/transforms.d.ts.map +1 -0
  51. package/dist/transforms.js +155 -0
  52. package/dist/transforms.js.map +1 -0
  53. package/dist/types.d.ts +129 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +20 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +56 -0
  58. package/src/__tests__/conflict.test.ts +325 -0
  59. package/src/blobs.ts +187 -0
  60. package/src/conflict.ts +92 -0
  61. package/src/index.ts +30 -0
  62. package/src/kysely-serialize.ts +214 -0
  63. package/src/logger.ts +80 -0
  64. package/src/proxy/index.ts +10 -0
  65. package/src/proxy/types.ts +57 -0
  66. package/src/schemas/blobs.ts +101 -0
  67. package/src/schemas/common.ts +45 -0
  68. package/src/schemas/index.ts +7 -0
  69. package/src/schemas/sync.ts +222 -0
  70. package/src/scopes/index.ts +122 -0
  71. package/src/transforms.ts +256 -0
  72. package/src/types.ts +158 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @syncular/core - Sync protocol Zod schemas
3
+ *
4
+ * These schemas define the sync protocol types and can be used for:
5
+ * - Runtime validation
6
+ * - OpenAPI spec generation
7
+ * - Type inference
8
+ */
9
+
10
+ import { z } from 'zod';
11
+
12
+ // ============================================================================
13
+ // Operation Types
14
+ // ============================================================================
15
+
16
+ export const SyncOpSchema = z.enum(['upsert', 'delete']);
17
+ export type SyncOp = z.infer<typeof SyncOpSchema>;
18
+
19
+ // ============================================================================
20
+ // Scope Schemas
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Stored scopes on a change (single values only)
25
+ */
26
+ const StoredScopesSchema = z.record(z.string(), z.string());
27
+
28
+ /**
29
+ * Scope values in a subscription request (can be arrays)
30
+ */
31
+ export const ScopeValuesSchema = z.record(
32
+ z.string(),
33
+ z.union([z.string(), z.array(z.string())])
34
+ );
35
+
36
+ // ============================================================================
37
+ // Sync Operation Schema
38
+ // ============================================================================
39
+
40
+ export const SyncOperationSchema = z.object({
41
+ table: z.string(),
42
+ row_id: z.string(),
43
+ op: SyncOpSchema,
44
+ payload: z.record(z.string(), z.unknown()).nullable(),
45
+ base_version: z.number().int().nullable().optional(),
46
+ });
47
+
48
+ export type SyncOperation = z.infer<typeof SyncOperationSchema>;
49
+
50
+ // ============================================================================
51
+ // Push Request/Response Schemas
52
+ // ============================================================================
53
+
54
+ export const SyncPushRequestSchema = z.object({
55
+ clientId: z.string().min(1),
56
+ clientCommitId: z.string().min(1),
57
+ operations: z.array(SyncOperationSchema).min(1),
58
+ schemaVersion: z.number().int().min(1),
59
+ });
60
+
61
+ export type SyncPushRequest = z.infer<typeof SyncPushRequestSchema>;
62
+
63
+ const SyncOperationResultAppliedSchema = z.object({
64
+ opIndex: z.number().int(),
65
+ status: z.literal('applied'),
66
+ });
67
+
68
+ const SyncOperationResultConflictSchema = z.object({
69
+ opIndex: z.number().int(),
70
+ status: z.literal('conflict'),
71
+ message: z.string(),
72
+ server_version: z.number().int(),
73
+ server_row: z.unknown(),
74
+ });
75
+
76
+ const SyncOperationResultErrorSchema = z.object({
77
+ opIndex: z.number().int(),
78
+ status: z.literal('error'),
79
+ error: z.string(),
80
+ code: z.string().optional(),
81
+ retriable: z.boolean().optional(),
82
+ });
83
+
84
+ export const SyncOperationResultSchema = z.union([
85
+ SyncOperationResultAppliedSchema,
86
+ SyncOperationResultConflictSchema,
87
+ SyncOperationResultErrorSchema,
88
+ ]);
89
+
90
+ export type SyncOperationResult = z.infer<typeof SyncOperationResultSchema>;
91
+
92
+ export const SyncPushResponseSchema = z.object({
93
+ ok: z.literal(true),
94
+ status: z.enum(['applied', 'cached', 'rejected']),
95
+ commitSeq: z.number().int().optional(),
96
+ results: z.array(SyncOperationResultSchema),
97
+ });
98
+
99
+ export type SyncPushResponse = z.infer<typeof SyncPushResponseSchema>;
100
+
101
+ // ============================================================================
102
+ // Bootstrap State Schema
103
+ // ============================================================================
104
+
105
+ export const SyncBootstrapStateSchema = z.object({
106
+ asOfCommitSeq: z.number().int(),
107
+ tables: z.array(z.string()),
108
+ tableIndex: z.number().int(),
109
+ rowCursor: z.string().nullable(),
110
+ });
111
+
112
+ export type SyncBootstrapState = z.infer<typeof SyncBootstrapStateSchema>;
113
+
114
+ // ============================================================================
115
+ // Pull Request/Response Schemas
116
+ // ============================================================================
117
+
118
+ export const SyncSubscriptionRequestSchema = z.object({
119
+ id: z.string().min(1),
120
+ shape: z.string().min(1),
121
+ scopes: ScopeValuesSchema,
122
+ params: z.record(z.string(), z.unknown()).optional(),
123
+ cursor: z.number().int(),
124
+ bootstrapState: SyncBootstrapStateSchema.nullable().optional(),
125
+ });
126
+
127
+ export type SyncSubscriptionRequest = z.infer<
128
+ typeof SyncSubscriptionRequestSchema
129
+ >;
130
+
131
+ export const SyncPullRequestSchema = z.object({
132
+ clientId: z.string().min(1),
133
+ limitCommits: z.number().int().min(1),
134
+ limitSnapshotRows: z.number().int().min(1).optional(),
135
+ maxSnapshotPages: z.number().int().min(1).optional(),
136
+ dedupeRows: z.boolean().optional(),
137
+ subscriptions: z.array(SyncSubscriptionRequestSchema),
138
+ });
139
+
140
+ export type SyncPullRequest = z.infer<typeof SyncPullRequestSchema>;
141
+
142
+ export const SyncChangeSchema = z.object({
143
+ table: z.string(),
144
+ row_id: z.string(),
145
+ op: SyncOpSchema,
146
+ row_json: z.unknown().nullable(),
147
+ row_version: z.number().int().nullable(),
148
+ scopes: StoredScopesSchema,
149
+ });
150
+
151
+ export type SyncChange = z.infer<typeof SyncChangeSchema>;
152
+
153
+ export const SyncCommitSchema = z.object({
154
+ commitSeq: z.number().int(),
155
+ createdAt: z.string(),
156
+ actorId: z.string(),
157
+ changes: z.array(SyncChangeSchema),
158
+ });
159
+
160
+ export type SyncCommit = z.infer<typeof SyncCommitSchema>;
161
+
162
+ export const SyncSnapshotChunkRefSchema = z.object({
163
+ id: z.string(),
164
+ byteLength: z.number().int(),
165
+ sha256: z.string(),
166
+ encoding: z.literal('ndjson'),
167
+ compression: z.literal('gzip'),
168
+ });
169
+
170
+ export type SyncSnapshotChunkRef = z.infer<typeof SyncSnapshotChunkRefSchema>;
171
+
172
+ export const SyncSnapshotSchema = z.object({
173
+ table: z.string(),
174
+ rows: z.array(z.unknown()),
175
+ chunks: z.array(SyncSnapshotChunkRefSchema).optional(),
176
+ isFirstPage: z.boolean(),
177
+ isLastPage: z.boolean(),
178
+ });
179
+
180
+ export type SyncSnapshot = z.infer<typeof SyncSnapshotSchema>;
181
+
182
+ export const SyncPullSubscriptionResponseSchema = z.object({
183
+ id: z.string(),
184
+ status: z.enum(['active', 'revoked']),
185
+ scopes: ScopeValuesSchema,
186
+ bootstrap: z.boolean(),
187
+ bootstrapState: SyncBootstrapStateSchema.nullable().optional(),
188
+ nextCursor: z.number().int(),
189
+ commits: z.array(SyncCommitSchema),
190
+ snapshots: z.array(SyncSnapshotSchema).optional(),
191
+ });
192
+
193
+ export type SyncPullSubscriptionResponse = z.infer<
194
+ typeof SyncPullSubscriptionResponseSchema
195
+ >;
196
+
197
+ export const SyncPullResponseSchema = z.object({
198
+ ok: z.literal(true),
199
+ subscriptions: z.array(SyncPullSubscriptionResponseSchema),
200
+ });
201
+
202
+ export type SyncPullResponse = z.infer<typeof SyncPullResponseSchema>;
203
+
204
+ // ============================================================================
205
+ // Combined Sync Request/Response Schemas
206
+ // ============================================================================
207
+
208
+ export const SyncCombinedRequestSchema = z.object({
209
+ clientId: z.string().min(1),
210
+ push: SyncPushRequestSchema.omit({ clientId: true }).optional(),
211
+ pull: SyncPullRequestSchema.omit({ clientId: true }).optional(),
212
+ });
213
+
214
+ export type SyncCombinedRequest = z.infer<typeof SyncCombinedRequestSchema>;
215
+
216
+ export const SyncCombinedResponseSchema = z.object({
217
+ ok: z.literal(true),
218
+ push: SyncPushResponseSchema.optional(),
219
+ pull: SyncPullResponseSchema.optional(),
220
+ });
221
+
222
+ export type SyncCombinedResponse = z.infer<typeof SyncCombinedResponseSchema>;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @syncular/core - Scope types, patterns, and utilities
3
+ *
4
+ * Scope patterns define how data is partitioned for sync.
5
+ * Scopes are stored as JSONB on changes for flexible filtering.
6
+ * Patterns use `{placeholder}` syntax to extract or inject values.
7
+ */
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Scope pattern string, e.g., 'user:{user_id}', 'project:{project_id}'
13
+ */
14
+ export type ScopePattern = string;
15
+
16
+ /**
17
+ * Scope values - the actual values for scope variables.
18
+ * Values can be single strings or arrays (for multi-value subscriptions).
19
+ *
20
+ * @example
21
+ * { user_id: 'U1' }
22
+ * { project_id: ['P1', 'P2'] }
23
+ * { year: '2025', month: '03' }
24
+ */
25
+ export type ScopeValues = Record<string, string | string[]>;
26
+
27
+ /**
28
+ * Stored scopes on a change - always single values (not arrays).
29
+ * This is what gets stored in the JSONB column.
30
+ *
31
+ * @example
32
+ * { user_id: 'U1', project_id: 'P1' }
33
+ */
34
+ export type StoredScopes = Record<string, string>;
35
+
36
+ /**
37
+ * Simplified scope definition.
38
+ * Can be a simple pattern string or an object with explicit column mapping.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // Simple: pattern column is auto-derived
43
+ * scopes: ['user:{user_id}', 'org:{org_id}']
44
+ *
45
+ * // Explicit: when column differs from pattern variable
46
+ * scopes: [
47
+ * { pattern: 'user:{user_id}', column: 'owner_id' }
48
+ * ]
49
+ * ```
50
+ */
51
+ export type ScopeDefinition = string | { pattern: string; column: string };
52
+
53
+ // ── Pattern parsing (internal helpers) ───────────────────────────────
54
+
55
+ /**
56
+ * Extract the placeholder name from a pattern.
57
+ * Returns null if the pattern doesn't contain a valid placeholder.
58
+ */
59
+ function extractPlaceholder(pattern: string): {
60
+ prefix: string;
61
+ placeholder: string;
62
+ suffix: string;
63
+ } | null {
64
+ const match = pattern.match(/^(.*?)\{(\w+)\}(.*)$/);
65
+ if (!match) return null;
66
+
67
+ return {
68
+ prefix: match[1]!,
69
+ placeholder: match[2]!,
70
+ suffix: match[3]!,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Extract the placeholder name from a pattern.
76
+ */
77
+ function getPlaceholderName(pattern: string): string | null {
78
+ const parsed = extractPlaceholder(pattern);
79
+ return parsed?.placeholder ?? null;
80
+ }
81
+
82
+ /**
83
+ * Normalize scope definitions to a pattern-to-column map.
84
+ *
85
+ * @example
86
+ * normalizeScopes(['user:{user_id}'])
87
+ * // → { 'user:{user_id}': 'user_id' }
88
+ */
89
+ export function normalizeScopes(
90
+ scopes: ScopeDefinition[]
91
+ ): Record<string, string> {
92
+ const result: Record<string, string> = {};
93
+ for (const scope of scopes) {
94
+ if (typeof scope === 'string') {
95
+ const placeholder = getPlaceholderName(scope);
96
+ if (!placeholder) {
97
+ throw new Error(
98
+ `Scope pattern "${scope}" must contain a placeholder like {column_name}`
99
+ );
100
+ }
101
+ result[scope] = placeholder;
102
+ } else {
103
+ result[scope.pattern] = scope.column;
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+
109
+ // ── Value operations (public) ────────────────────────────────────────
110
+
111
+ /**
112
+ * Extract variable names from a scope pattern.
113
+ *
114
+ * @example
115
+ * extractScopeVars('project:{project_id}') // ['project_id']
116
+ * extractScopeVars('event_date:{year}:{month}') // ['year', 'month']
117
+ */
118
+ export function extractScopeVars(pattern: ScopePattern): string[] {
119
+ const matches = pattern.match(/\{([^}]+)\}/g);
120
+ if (!matches) return [];
121
+ return matches.map((m) => m.slice(1, -1));
122
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @syncular/core - Data transformation hooks
3
+ *
4
+ * Provides interfaces for field-level transformations (e.g., encryption/decryption)
5
+ * that can be applied during sync operations.
6
+ */
7
+
8
+ /**
9
+ * Direction of the transformation.
10
+ * - 'toClient': Server → Client (e.g., decrypt for client)
11
+ * - 'toServer': Client → Server (e.g., encrypt for server)
12
+ */
13
+ export type TransformDirection = 'toClient' | 'toServer';
14
+
15
+ /**
16
+ * Context passed to transform functions.
17
+ */
18
+ export interface TransformContext {
19
+ /** Direction of transformation */
20
+ direction: TransformDirection;
21
+ /** Scope name */
22
+ scope: string;
23
+ /** Table name */
24
+ table: string;
25
+ /** Row ID */
26
+ rowId: string;
27
+ /** User ID performing the operation */
28
+ userId: string;
29
+ }
30
+
31
+ /**
32
+ * A field transformer handles transformation of a single field.
33
+ *
34
+ * @example
35
+ * const secretNotesTransformer: FieldTransformer = {
36
+ * field: 'secret_notes',
37
+ * async transform(value, ctx) {
38
+ * const key = await getUserEncryptionKey(ctx.userId);
39
+ * return ctx.direction === 'toClient'
40
+ * ? decrypt(value as string, key)
41
+ * : encrypt(value as string, key);
42
+ * }
43
+ * };
44
+ */
45
+ export interface FieldTransformer {
46
+ /** Field name to transform */
47
+ field: string;
48
+ /**
49
+ * Transform the field value.
50
+ * @param value - Current field value
51
+ * @param ctx - Transform context
52
+ * @returns Transformed value
53
+ */
54
+ transform(value: unknown, ctx: TransformContext): Promise<unknown> | unknown;
55
+ }
56
+
57
+ /**
58
+ * Configuration for transforms on a scope.
59
+ */
60
+ export interface ScopeTransformConfig {
61
+ /** Scope name this config applies to */
62
+ scope: string;
63
+ /** Field transformers for this scope */
64
+ fields?: FieldTransformer[];
65
+ }
66
+
67
+ /**
68
+ * Registry for managing data transforms.
69
+ *
70
+ * @example
71
+ * const transforms = new TransformRegistry();
72
+ *
73
+ * transforms.register({
74
+ * scope: 'tasks',
75
+ * fields: [{
76
+ * field: 'secret_notes',
77
+ * async transform(value, ctx) {
78
+ * const key = await getUserEncryptionKey(ctx.userId);
79
+ * return ctx.direction === 'toClient'
80
+ * ? decrypt(value as string, key)
81
+ * : encrypt(value as string, key);
82
+ * }
83
+ * }]
84
+ * });
85
+ *
86
+ * // Apply transforms to data
87
+ * const transformed = await transforms.apply(
88
+ * [{ id: '1', secret_notes: 'encrypted...' }],
89
+ * { direction: 'toClient', scope: 'tasks', ... }
90
+ * );
91
+ */
92
+ export class TransformRegistry {
93
+ private configs: Map<string, ScopeTransformConfig> = new Map();
94
+
95
+ /**
96
+ * Register transform config for a scope.
97
+ * @throws If config for this scope is already registered
98
+ */
99
+ register(config: ScopeTransformConfig): void {
100
+ if (this.configs.has(config.scope)) {
101
+ throw new Error(
102
+ `Transform config for scope "${config.scope}" is already registered`
103
+ );
104
+ }
105
+ this.configs.set(config.scope, config);
106
+ }
107
+
108
+ /**
109
+ * Unregister transform config by scope.
110
+ * @returns true if config was found and removed
111
+ */
112
+ unregister(scope: string): boolean {
113
+ return this.configs.delete(scope);
114
+ }
115
+
116
+ /**
117
+ * Get config for a scope.
118
+ */
119
+ get(scope: string): ScopeTransformConfig | undefined {
120
+ return this.configs.get(scope);
121
+ }
122
+
123
+ /**
124
+ * Check if any transforms are registered for a scope.
125
+ */
126
+ hasTransforms(scope: string): boolean {
127
+ const config = this.configs.get(scope);
128
+ return config !== undefined && (config.fields?.length ?? 0) > 0;
129
+ }
130
+
131
+ /**
132
+ * Get all registered configs.
133
+ */
134
+ getAll(): ScopeTransformConfig[] {
135
+ return Array.from(this.configs.values());
136
+ }
137
+
138
+ /**
139
+ * Apply transforms to a single row.
140
+ *
141
+ * @param row - Row data to transform
142
+ * @param ctx - Transform context (without rowId, will be extracted from row)
143
+ * @param rowIdField - Field name for row ID (default: 'id')
144
+ * @returns Transformed row
145
+ */
146
+ async applyToRow<T extends Record<string, unknown>>(
147
+ row: T,
148
+ ctx: Omit<TransformContext, 'rowId'>,
149
+ rowIdField = 'id'
150
+ ): Promise<T> {
151
+ const config = this.configs.get(ctx.scope);
152
+ if (!config?.fields?.length) {
153
+ return row;
154
+ }
155
+
156
+ const rowId = String(row[rowIdField] ?? '');
157
+ const fullCtx: TransformContext = { ...ctx, rowId };
158
+ const result = { ...row };
159
+
160
+ for (const transformer of config.fields) {
161
+ if (transformer.field in result) {
162
+ try {
163
+ result[transformer.field as keyof T] = (await transformer.transform(
164
+ result[transformer.field],
165
+ fullCtx
166
+ )) as T[keyof T];
167
+ } catch (err) {
168
+ console.error(
169
+ `[transforms] Error transforming field "${transformer.field}" for ${ctx.scope}:${rowId}:`,
170
+ err
171
+ );
172
+ // Keep original value on error
173
+ }
174
+ }
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Apply transforms to multiple rows.
182
+ *
183
+ * @param rows - Array of rows to transform
184
+ * @param ctx - Transform context (without rowId)
185
+ * @param rowIdField - Field name for row ID (default: 'id')
186
+ * @returns Transformed rows
187
+ */
188
+ async apply<T extends Record<string, unknown>>(
189
+ rows: T[],
190
+ ctx: Omit<TransformContext, 'rowId'>,
191
+ rowIdField = 'id'
192
+ ): Promise<T[]> {
193
+ const config = this.configs.get(ctx.scope);
194
+ if (!config?.fields?.length) {
195
+ return rows;
196
+ }
197
+
198
+ return Promise.all(
199
+ rows.map((row) => this.applyToRow(row, ctx, rowIdField))
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Apply transforms to a mutation payload.
205
+ *
206
+ * @param payload - Mutation payload (may be partial row)
207
+ * @param ctx - Full transform context
208
+ * @returns Transformed payload
209
+ */
210
+ async applyToPayload<T extends Record<string, unknown>>(
211
+ payload: T | null,
212
+ ctx: TransformContext
213
+ ): Promise<T | null> {
214
+ if (!payload) return null;
215
+
216
+ const config = this.configs.get(ctx.scope);
217
+ if (!config?.fields?.length) {
218
+ return payload;
219
+ }
220
+
221
+ const result = { ...payload };
222
+
223
+ for (const transformer of config.fields) {
224
+ if (transformer.field in result) {
225
+ try {
226
+ result[transformer.field as keyof T] = (await transformer.transform(
227
+ result[transformer.field],
228
+ ctx
229
+ )) as T[keyof T];
230
+ } catch (err) {
231
+ console.error(
232
+ `[transforms] Error transforming field "${transformer.field}" for ${ctx.scope}:${ctx.rowId}:`,
233
+ err
234
+ );
235
+ // Keep original value on error
236
+ }
237
+ }
238
+ }
239
+
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * Clear all registered configs.
245
+ */
246
+ clear(): void {
247
+ this.configs.clear();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create a new transform registry.
253
+ */
254
+ export function createTransformRegistry(): TransformRegistry {
255
+ return new TransformRegistry();
256
+ }