@syncular/client 0.15.47 → 0.16.1

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/dist/remote.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Database-less SSP2 producer (§6.10). It prepares and sends ordinary commits
3
3
  * through the existing push path without creating a local replica or outbox.
4
4
  */
5
- import { decodeMessage, decodeRemoteOperationResponse, decodeRemoteOperationRealtimeMessage, encodeMessage, encodeRemoteOperationRequest, encodeRemoteOperationRealtimeMessage, encodeRow, PROTOCOL_WIRE_VERSION, } from '@syncular/core';
5
+ import { decodeMessage, decodeRemoteOperationResponse, decodeRemoteOperationRealtimeMessage, encodeMessage, encodeRemoteOperationRequest, encodeRemoteOperationRealtimeMessage, encodeRow, } from '@syncular/core';
6
6
  import { encryptRowValues } from './encryption.js';
7
7
  import { ClientSyncError } from './errors.js';
8
8
  import { compileClientSchema, recordToRowValues, } from './schema.js';
@@ -71,10 +71,14 @@ export class SyncRemoteClient {
71
71
  #operationSocketGeneration = 0;
72
72
  #watches = new Map();
73
73
  #encryption;
74
+ #logEpoch;
74
75
  constructor(config) {
75
76
  if (config.clientId.length === 0) {
76
77
  throw invalid('SyncRemoteClient clientId must be non-empty');
77
78
  }
79
+ if (config.logEpoch !== undefined && config.logEpoch.length === 0) {
80
+ throw invalid('SyncRemoteClient logEpoch must be non-empty');
81
+ }
78
82
  this.#schema =
79
83
  config.schema === undefined
80
84
  ? undefined
@@ -84,6 +88,7 @@ export class SyncRemoteClient {
84
88
  this.#operations = config.operations;
85
89
  this.#operationRealtime = config.operationRealtime;
86
90
  this.#encryption = config.encryption;
91
+ this.#logEpoch = config.logEpoch;
87
92
  }
88
93
  async prepareCommit(input) {
89
94
  const schema = this.#schema;
@@ -137,13 +142,16 @@ export class SyncRemoteClient {
137
142
  return {
138
143
  requestId: input.requestId,
139
144
  bytes: encodeMessage({
140
- wireVersion: PROTOCOL_WIRE_VERSION,
145
+ wireVersion: this.#logEpoch === undefined ? 1 : 2,
141
146
  msgKind: 'request',
142
147
  frames: [
143
148
  {
144
149
  type: 'REQ_HEADER',
145
150
  clientId: this.#clientId,
146
151
  schemaVersion: schema.version,
152
+ ...(this.#logEpoch !== undefined
153
+ ? { logEpoch: this.#logEpoch }
154
+ : {}),
147
155
  },
148
156
  {
149
157
  type: 'PUSH_COMMIT',
@@ -0,0 +1,23 @@
1
+ import type { WakeReason } from '@syncular/core';
2
+ import type { SecurityLifecycle } from './client.js';
3
+ import type { SyncIntent } from './invalidation.js';
4
+ export interface SyncSchedulerClient {
5
+ readonly syncNeeded: boolean;
6
+ readonly securityLifecycle: SecurityLifecycle;
7
+ syncUntilIdle(maxRounds?: number): Promise<unknown>;
8
+ onSyncNeeded(listener: (reason: 'startup' | 'hello' | WakeReason) => void): () => void;
9
+ onSyncIntent(listener: (intent: SyncIntent) => void): () => void;
10
+ }
11
+ export interface SyncSchedulerOptions {
12
+ readonly maxRounds?: number;
13
+ readonly onError?: (error: unknown) => void;
14
+ readonly now?: () => number;
15
+ readonly queueMicrotask?: (callback: () => void) => void;
16
+ readonly schedule?: (callback: () => void, delayMs: number) => () => void;
17
+ }
18
+ export interface SyncScheduler {
19
+ readonly stopped: boolean;
20
+ stop(): void;
21
+ }
22
+ /** Install the event-driven single-flight host loop for a direct client. */
23
+ export declare function installSyncScheduler(client: SyncSchedulerClient, options?: SyncSchedulerOptions): SyncScheduler;
@@ -0,0 +1,122 @@
1
+ /** Install the event-driven single-flight host loop for a direct client. */
2
+ export function installSyncScheduler(client, options = {}) {
3
+ const now = options.now ?? Date.now;
4
+ const enqueue = options.queueMicrotask ?? globalThis.queueMicrotask;
5
+ const schedule = options.schedule ??
6
+ ((callback, delayMs) => {
7
+ const timer = globalThis.setTimeout(callback, delayMs);
8
+ return () => globalThis.clearTimeout(timer);
9
+ });
10
+ let stopped = false;
11
+ let running = false;
12
+ let immediatePending = false;
13
+ let immediateQueued = false;
14
+ let backgroundReady = false;
15
+ let backgroundDue = Number.POSITIVE_INFINITY;
16
+ let cancelBackground;
17
+ const report = (error) => {
18
+ if (options.onError !== undefined) {
19
+ options.onError(error);
20
+ return;
21
+ }
22
+ const root = globalThis;
23
+ if (root.reportError !== undefined)
24
+ root.reportError(error);
25
+ else
26
+ console.error(error);
27
+ };
28
+ const clearBackground = () => {
29
+ cancelBackground?.();
30
+ cancelBackground = undefined;
31
+ backgroundReady = false;
32
+ backgroundDue = Number.POSITIVE_INFINITY;
33
+ };
34
+ const queueImmediate = () => {
35
+ immediatePending = true;
36
+ if (stopped || running || immediateQueued)
37
+ return;
38
+ immediateQueued = true;
39
+ enqueue(() => {
40
+ immediateQueued = false;
41
+ if (stopped || !immediatePending)
42
+ return;
43
+ immediatePending = false;
44
+ run();
45
+ });
46
+ };
47
+ const run = () => {
48
+ if (stopped || running)
49
+ return;
50
+ if (client.securityLifecycle === 'preflight') {
51
+ immediatePending = false;
52
+ return;
53
+ }
54
+ running = true;
55
+ backgroundReady = false;
56
+ void client
57
+ .syncUntilIdle(options.maxRounds)
58
+ .catch((error) => {
59
+ if (!stopped)
60
+ report(error);
61
+ })
62
+ .finally(() => {
63
+ running = false;
64
+ if (stopped)
65
+ return;
66
+ if (immediatePending || backgroundReady) {
67
+ queueImmediate();
68
+ return;
69
+ }
70
+ if (cancelBackground !== undefined && backgroundDue <= now()) {
71
+ clearBackground();
72
+ queueImmediate();
73
+ }
74
+ });
75
+ };
76
+ const consume = (intent) => {
77
+ if (stopped)
78
+ return;
79
+ if (intent.kind === 'none') {
80
+ clearBackground();
81
+ immediatePending = false;
82
+ return;
83
+ }
84
+ if (intent.kind === 'interactive') {
85
+ clearBackground();
86
+ queueImmediate();
87
+ return;
88
+ }
89
+ if (immediatePending || immediateQueued)
90
+ return;
91
+ clearBackground();
92
+ backgroundDue = now() + Math.max(0, intent.delayMs);
93
+ cancelBackground = schedule(() => {
94
+ cancelBackground = undefined;
95
+ backgroundDue = Number.POSITIVE_INFINITY;
96
+ backgroundReady = true;
97
+ if (!running)
98
+ queueImmediate();
99
+ }, Math.max(0, intent.delayMs));
100
+ };
101
+ const unsubscribeNeeded = client.onSyncNeeded(() => {
102
+ consume({ kind: 'interactive' });
103
+ });
104
+ const unsubscribeIntent = client.onSyncIntent(consume);
105
+ if (client.syncNeeded && client.securityLifecycle === 'active') {
106
+ consume({ kind: 'interactive' });
107
+ }
108
+ return {
109
+ get stopped() {
110
+ return stopped;
111
+ },
112
+ stop() {
113
+ if (stopped)
114
+ return;
115
+ stopped = true;
116
+ clearBackground();
117
+ immediatePending = false;
118
+ unsubscribeNeeded();
119
+ unsubscribeIntent();
120
+ },
121
+ };
122
+ }
package/dist/window.d.ts CHANGED
@@ -14,6 +14,11 @@
14
14
  */
15
15
  import { type ScopeMap } from '@syncular/core';
16
16
  import type { ClientDatabase } from './database.js';
17
+ export type TimeBucketUnit = 'month';
18
+ /** Derive the immutable UTC scope value stored when a row is created. */
19
+ export declare function creationTimeBucket(createdAtMs: number, unit: TimeBucketUnit): string;
20
+ /** Return a rolling UTC month window ordered from oldest to newest. */
21
+ export declare function last(count: number, unit: TimeBucketUnit, nowMs?: number): string[];
17
22
  /**
18
23
  * A window base: one table, one variable whose values are the window
19
24
  * units, and any FIXED scopes every unit shares (other variables pinned
package/dist/window.js CHANGED
@@ -13,6 +13,45 @@
13
13
  * transaction and the invalidation choke point.
14
14
  */
15
15
  import { canonicalScopeJson } from '@syncular/core';
16
+ import { ClientSyncError } from './errors.js';
17
+ const MAX_TIME_BUCKET_MS = 253_402_300_799_999;
18
+ function monthBucket(year, month) {
19
+ return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}`;
20
+ }
21
+ /** Derive the immutable UTC scope value stored when a row is created. */
22
+ export function creationTimeBucket(createdAtMs, unit) {
23
+ if (unit !== 'month' ||
24
+ !Number.isSafeInteger(createdAtMs) ||
25
+ createdAtMs < 0 ||
26
+ createdAtMs > MAX_TIME_BUCKET_MS) {
27
+ throw new ClientSyncError('sync.invalid_request', 'creationTimeBucket requires a supported unit and a UTC timestamp from 1970 through 9999');
28
+ }
29
+ const date = new Date(createdAtMs);
30
+ return monthBucket(date.getUTCFullYear(), date.getUTCMonth() + 1);
31
+ }
32
+ /** Return a rolling UTC month window ordered from oldest to newest. */
33
+ export function last(count, unit, nowMs = Date.now()) {
34
+ if (unit !== 'month' ||
35
+ !Number.isSafeInteger(count) ||
36
+ count < 1 ||
37
+ count > 1_200 ||
38
+ !Number.isSafeInteger(nowMs) ||
39
+ nowMs < 0 ||
40
+ nowMs > MAX_TIME_BUCKET_MS) {
41
+ throw new ClientSyncError('sync.invalid_request', 'last requires a supported unit, a count from 1 through 1200, and a UTC timestamp from 1970 through 9999');
42
+ }
43
+ const date = new Date(nowMs);
44
+ const current = date.getUTCFullYear() * 12 + date.getUTCMonth();
45
+ if (current - (count - 1) < 1970 * 12) {
46
+ throw new ClientSyncError('sync.invalid_request', 'last requires every returned UTC month to fall from 1970 through 9999');
47
+ }
48
+ const units = [];
49
+ for (let offset = count - 1; offset >= 0; offset -= 1) {
50
+ const value = current - offset;
51
+ units.push(monthBucket(Math.floor(value / 12), (value % 12) + 1));
52
+ }
53
+ return units;
54
+ }
16
55
  /**
17
56
  * A stable, server-opaque key for a window base — table + variable +
18
57
  * canonical fixed scopes. Two `setWindow` calls with the same base
@@ -91,7 +91,7 @@ export function startSyncWorker(overrides = {}) {
91
91
  autoSyncScheduled = false;
92
92
  if (closed ||
93
93
  client === undefined ||
94
- client.securityLifecycle === 'preflight') {
94
+ client.securityLifecycle() === 'preflight') {
95
95
  return;
96
96
  }
97
97
  const running = client;
@@ -113,7 +113,7 @@ export function startSyncWorker(overrides = {}) {
113
113
  if (!autoSync ||
114
114
  closed ||
115
115
  client === undefined ||
116
- client.securityLifecycle === 'preflight' ||
116
+ client.securityLifecycle() === 'preflight' ||
117
117
  intent.kind === 'none') {
118
118
  return;
119
119
  }
@@ -147,9 +147,6 @@ export function startSyncWorker(overrides = {}) {
147
147
  autoSyncScheduled = true;
148
148
  queueMicrotask(runAutoSync);
149
149
  }
150
- function consumeEffects(effects) {
151
- consumeSyncIntent(effects.sync);
152
- }
153
150
  function requireClient() {
154
151
  if (client === undefined) {
155
152
  throw new ClientSyncError(WORKER_FAILED_CODE, 'the worker received a call before init completed');
@@ -280,12 +277,12 @@ export function startSyncWorker(overrides = {}) {
280
277
  // `start()` may discover persisted subscriptions/outbox work. Its callback
281
278
  // fires before this worker publishes the initialized client, so consume
282
279
  // the durable state once here as well; coalescing makes this a single task.
283
- if (started.syncNeeded)
280
+ if (started.statusSnapshot().syncNeeded)
284
281
  consumeSyncIntent({ kind: 'interactive' });
285
282
  return { clientId: started.clientId };
286
283
  }
287
284
  const api = {
288
- securityLifecycle: () => requireClient().securityLifecycle,
285
+ securityLifecycle: () => requireClient().securityLifecycle(),
289
286
  beginSecurityPreflight: async () => {
290
287
  if (backgroundTimer !== undefined)
291
288
  clearTimeout(backgroundTimer);
@@ -304,20 +301,16 @@ export function startSyncWorker(overrides = {}) {
304
301
  subscribe: (input) => requireClient().subscribe(input),
305
302
  unsubscribe: (id) => requireClient().unsubscribe(id),
306
303
  setWindow: async (base, units) => {
307
- const result = await requireClient().setWindowCommand(base, units);
308
- consumeEffects(result.effects);
304
+ await requireClient().setWindowCommand(base, units);
309
305
  },
310
306
  windowState: (base) => requireClient().windowState(base),
311
307
  mutate: (mutations) => {
312
- const result = requireClient().mutateCommand(mutations);
313
- consumeEffects(result.effects);
314
- return result.value;
308
+ return requireClient().mutateCommand(mutations).value;
315
309
  },
316
310
  patch: (table, rowId, partial, options) => {
317
311
  // Same §8.4 rule as `mutate`: a local write must push without the app
318
312
  // orchestrating sync, so consume the core's immediate intent.
319
313
  const result = requireClient().patchCommand(table, rowId, partial, options);
320
- consumeEffects(result.effects);
321
314
  return result.value;
322
315
  },
323
316
  purgeLocalData: (input) => requireClient().purgeLocalData(input),
@@ -343,15 +336,11 @@ export function startSyncWorker(overrides = {}) {
343
336
  realtime: snapshot.host.realtime,
344
337
  });
345
338
  },
346
- conflicts: () => requireClient().conflicts,
347
- rejections: () => requireClient().rejections,
339
+ conflicts: () => requireClient().conflicts(),
340
+ rejections: () => requireClient().rejections(),
348
341
  commitOutcome: (clientCommitId) => requireClient().commitOutcome(clientCommitId),
349
342
  commitOutcomes: (query) => requireClient().commitOutcomes(query),
350
343
  resolveCommitOutcome: (input) => requireClient().resolveCommitOutcome(input),
351
- schemaFloor: () => requireClient().schemaFloor,
352
- leaseState: () => requireClient().leaseState,
353
- upgrading: () => requireClient().upgrading,
354
- syncNeeded: () => requireClient().syncNeeded,
355
344
  pendingCommits: () => requireClient().pendingCommits(),
356
345
  subscriptions: () => requireClient().subscriptions(),
357
346
  subscription: (id) => requireClient().subscription(id),
@@ -1,3 +1,4 @@
1
+ import type { PromiseMethods } from './client.js';
1
2
  /**
2
3
  * Main-thread side of the worker mode and the
3
4
  * multi-tab topology.
@@ -23,7 +24,7 @@
23
24
  */
24
25
  import type { WakeReason } from '@syncular/core';
25
26
  import type { BlobRef, CachedBlob } from './blob.js';
26
- import type { ConflictRecord, LeaseState, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SchemaFloor, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
27
+ import type { ConflictRecord, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
27
28
  import type { SqlRow, SqlValue } from './database.js';
28
29
  import { ClientDiagnosticsEmitter, type ClientDiagnosticsListener, type ClientDiagnosticsRequest, type ClientDiagnosticsSnapshot } from './diagnostics.js';
29
30
  import type { EncryptionKeyringConfig } from './encryption.js';
@@ -38,7 +39,7 @@ import type { CommitOutcome, CommitOutcomeQuery, ResolveCommitOutcomeInput } fro
38
39
  import type { ClientSchema } from './schema.js';
39
40
  import type { SubscriptionRecord } from './state.js';
40
41
  import type { WindowBase } from './window.js';
41
- import { type SyncWorkerEvent, type WorkerDatabaseInit, type WorkerEndpoints, type WorkerErrorShape, type WorkerSecurityActivation } from './worker-protocol.js';
42
+ import { type SyncWorkerEvent, type WorkerApi, type WorkerDatabaseInit, type WorkerEndpoints, type WorkerErrorShape, type WorkerSecurityActivation } from './worker-protocol.js';
42
43
  /** Classify startup without echoing a chunk URL or bundler text to UI/logs. */
43
44
  export declare function workerStartupError(message: unknown): ClientSyncError;
44
45
  export type HandleRole = 'leader' | 'follower';
@@ -132,7 +133,7 @@ interface LeaderCore {
132
133
  * promise. `role` is `'leader'` (owns the worker) or `'follower'` (proxies to
133
134
  * the leader over the channel). Constructed via {@link createSyncClientHandle}.
134
135
  */
135
- export declare class SyncClientHandle {
136
+ export declare class SyncClientHandle implements PromiseMethods<WorkerApi> {
136
137
  #private;
137
138
  /** True only for a leader handle. Kept for the pre-multiTab contract. */
138
139
  get isLeader(): boolean;
@@ -214,11 +215,7 @@ export declare class SyncClientHandle {
214
215
  commitOutcome(clientCommitId: string): Promise<CommitOutcome | undefined>;
215
216
  commitOutcomes(query?: CommitOutcomeQuery): Promise<readonly CommitOutcome[]>;
216
217
  resolveCommitOutcome(input: ResolveCommitOutcomeInput): Promise<CommitOutcome>;
217
- schemaFloor(): Promise<SchemaFloor | undefined>;
218
- leaseState(): Promise<LeaseState | undefined>;
219
218
  /** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
220
- upgrading(): Promise<boolean>;
221
- syncNeeded(): Promise<boolean>;
222
219
  pendingCommits(): Promise<OutboxCommit[]>;
223
220
  subscriptions(): Promise<SubscriptionRecord[]>;
224
221
  subscription(id: string): Promise<SubscriptionRecord | undefined>;
@@ -104,12 +104,12 @@ export class SyncClientHandle {
104
104
  ref: this,
105
105
  clientId: () => this.#clientId,
106
106
  role: () => this.#role,
107
- outbox: async () => (await this.pendingCommits()).length,
107
+ outbox: async () => (await this.statusSnapshot()).outbox,
108
108
  subscriptions: () => this.subscriptions(),
109
109
  conflicts: async () => (await this.conflicts()).length,
110
110
  rejections: async () => (await this.rejections()).length,
111
- syncNeeded: () => this.syncNeeded(),
112
- upgrading: () => this.upgrading(),
111
+ syncNeeded: async () => (await this.statusSnapshot()).syncNeeded,
112
+ upgrading: async () => (await this.statusSnapshot()).upgrading,
113
113
  onInvalidate: (listener) => this.onInvalidate(listener),
114
114
  });
115
115
  }
@@ -310,19 +310,7 @@ export class SyncClientHandle {
310
310
  resolveCommitOutcome(input) {
311
311
  return this.#call('resolveCommitOutcome', [input]);
312
312
  }
313
- schemaFloor() {
314
- return this.#call('schemaFloor', []);
315
- }
316
- leaseState() {
317
- return this.#call('leaseState', []);
318
- }
319
313
  /** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
320
- upgrading() {
321
- return this.#call('upgrading', []);
322
- }
323
- syncNeeded() {
324
- return this.#call('syncNeeded', []);
325
- }
326
314
  pendingCommits() {
327
315
  return this.#call('pendingCommits', []);
328
316
  }
@@ -18,15 +18,14 @@
18
18
  */
19
19
  import type { WakeReason } from '@syncular/core';
20
20
  import type { BlobRef, CachedBlob } from './blob.js';
21
- import type { ConflictRecord, LeaseState, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, RejectionRecord, SchemaFloor, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
21
+ import type { ConflictRecord, ClientSnapshotMethods, MutationInput, PresencePeer, QueryReadSpec, QuerySnapshot, SecurityLifecycle, SubscribeInput, SyncClientLimits, SyncSummary, WindowState } from './client.js';
22
22
  import type { SqlRow, SqlValue } from './database.js';
23
- import type { ClientDiagnosticsRequest, ClientDiagnosticsSnapshot } from './diagnostics.js';
23
+ import type { ClientDiagnosticsSnapshot } from './diagnostics.js';
24
24
  import type { EncryptionKeyringConfig } from './encryption.js';
25
- import type { ClientChangeBatch, LocalRevision, SyncStatusSnapshot } from './invalidation.js';
25
+ import type { ClientChangeBatch, LocalRevision } from './invalidation.js';
26
26
  import type { LocalDataPurgeInput, LocalDataPurgeResult } from './local-purge.js';
27
27
  import type { LocalDataRebootstrapInput, LocalDataRebootstrapResult } from './local-rebootstrap.js';
28
28
  import type { OutboxCommit } from './outbox.js';
29
- import type { CommitOutcome, CommitOutcomeQuery, ResolveCommitOutcomeInput } from './outcomes.js';
30
29
  import type { ClientSchema } from './schema.js';
31
30
  import type { SubscriptionRecord } from './state.js';
32
31
  import type { WindowBase } from './window.js';
@@ -88,7 +87,7 @@ export interface WorkerInitResult {
88
87
  export interface WorkerSecurityActivation {
89
88
  readonly encryption?: EncryptionKeyringConfig;
90
89
  }
91
- export interface WorkerApi {
90
+ export interface WorkerApi extends Omit<ClientSnapshotMethods, 'querySnapshot'> {
92
91
  securityLifecycle(): SecurityLifecycle;
93
92
  beginSecurityPreflight(): Promise<void>;
94
93
  activateSecurity(options?: WorkerSecurityActivation): Promise<void>;
@@ -112,19 +111,6 @@ export interface WorkerApi {
112
111
  query(sql: string, params?: readonly SqlValue[]): SqlRow[];
113
112
  querySnapshot(spec: QueryReadSpec): QuerySnapshot;
114
113
  localRevision(): LocalRevision;
115
- statusSnapshot(): SyncStatusSnapshot;
116
- diagnosticsSnapshot(request?: ClientDiagnosticsRequest): ClientDiagnosticsSnapshot;
117
- conflicts(): readonly ConflictRecord[];
118
- rejections(): readonly RejectionRecord[];
119
- commitOutcome(clientCommitId: string): CommitOutcome | undefined;
120
- commitOutcomes(query?: CommitOutcomeQuery): readonly CommitOutcome[];
121
- resolveCommitOutcome(input: ResolveCommitOutcomeInput): CommitOutcome;
122
- schemaFloor(): SchemaFloor | undefined;
123
- /** §7.3.5: the opaque auth-lease state, or undefined. */
124
- leaseState(): LeaseState | undefined;
125
- /** §7.4.5: true while a schema-bump reset + first re-bootstrap runs. */
126
- upgrading(): boolean;
127
- syncNeeded(): boolean;
128
114
  pendingCommits(): OutboxCommit[];
129
115
  subscriptions(): SubscriptionRecord[];
130
116
  subscription(id: string): SubscriptionRecord | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client",
3
- "version": "0.15.47",
3
+ "version": "0.16.1",
4
4
  "description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -34,14 +34,6 @@
34
34
  "default": "./dist/index.js"
35
35
  }
36
36
  },
37
- "./realtime-supervisor-observation": {
38
- "bun": "./src/realtime-supervisor-observation.ts",
39
- "browser": "./dist/realtime-supervisor-observation.js",
40
- "import": {
41
- "types": "./dist/realtime-supervisor-observation.d.ts",
42
- "default": "./dist/realtime-supervisor-observation.js"
43
- }
44
- },
45
37
  "./bun": {
46
38
  "bun": "./src/bun-database.ts",
47
39
  "browser": "./dist/bun-database.js",
@@ -97,9 +89,9 @@
97
89
  },
98
90
  "dependencies": {
99
91
  "@sqlite.org/sqlite-wasm": "^3.53.0-build1",
100
- "@syncular/core": "0.15.47"
92
+ "@syncular/core": "0.16.1"
101
93
  },
102
94
  "devDependencies": {
103
- "@syncular/server": "0.15.47"
95
+ "@syncular/server": "0.16.1"
104
96
  }
105
97
  }
@@ -15,6 +15,12 @@ import {
15
15
  type SqlValue,
16
16
  } from './database';
17
17
 
18
+ declare module 'bun:sqlite' {
19
+ interface Database {
20
+ clearQueryCache(): void;
21
+ }
22
+ }
23
+
18
24
  type BunParam = string | number | bigint | Uint8Array | null;
19
25
 
20
26
  function coerceParams(params: readonly SqlValue[]): BunParam[] {
@@ -34,6 +40,11 @@ export class BunClientDatabase implements ClientDatabase {
34
40
 
35
41
  exec(sql: string, params: readonly SqlValue[] = []): void {
36
42
  this.db.query(sql).run(...coerceParams(params));
43
+ // `Database.query()` caches prepared statements. Clear that cache after
44
+ // schema DDL so a reset does not reprepare every later row upsert.
45
+ if (/^\s*(?:CREATE|DROP|ALTER)\b/i.test(sql)) {
46
+ this.db.clearQueryCache();
47
+ }
37
48
  }
38
49
 
39
50
  query(sql: string, params: readonly SqlValue[] = []): SqlRow[] {