@syncular/server-workers 0.15.48 → 0.17.0

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 CHANGED
@@ -101,21 +101,23 @@ See `wrangler.toml.example` for the binding config.
101
101
 
102
102
  ## Schema migration (D1)
103
103
 
104
- `D1ServerStorage` does **not** apply its DDL on construction (a cold request
105
- must never race a schema apply). Apply it once with wrangler. Generate the
106
- migration SQL from `sqliteDdlStatements()` (exported from `@syncular/server`)
107
- into a `migrations/` file, then:
108
-
109
- ```sh
110
- wrangler d1 create syncular
111
- wrangler d1 migrations apply syncular
112
- ```
113
-
114
- The schema is plain SQLite DDL (shared with `bun:sqlite` via
115
- `sqlite-dialect.ts`), so it is portable across the two SQLite-family
116
- storages. Regenerate and apply a migration when upgrading Syncular adds a core
117
- table. The durable reaction queue uses `sync_reactions`; a Worker running a
118
- reaction planner or runner fails closed if that migration is absent.
104
+ Run `storage.migrateSchema(compileSchema(schema))` from an authenticated
105
+ maintenance handler before admitting sync traffic. The method creates core
106
+ and application tables and rewrites stored rows. It returns
107
+ `{ complete, statementsExecuted }`; call it again in a new Worker invocation
108
+ when `complete` is false.
109
+
110
+ Each call defaults to 50 statements. Set `{ maxStatements: 40 }` to leave
111
+ room for other queries, or choose another integer from 10 through 1000 for
112
+ your D1 plan. Run at most one call per invocation. Progress and row updates
113
+ commit together, so interrupted requests can resume with the same schema.
114
+ The storage rejects row reads and transaction commits during migration.
115
+
116
+ `ensureSchema` performs one step and throws
117
+ `sync.storage.schema_migration_pending` when another invocation is needed.
118
+ Drain older Syncular deployments before starting an upgrade: they do not
119
+ check the migration state. See [the migration guide](https://syncular.dev/server-workers/#schema-migration)
120
+ for a maintenance handler and the remaining D1 limits.
119
121
 
120
122
  ## Storage: D1 (`D1ServerStorage`)
121
123
 
@@ -139,6 +141,10 @@ from a Worker scheduler, queue consumer, Workflow step, or Durable Object
139
141
  alarm according to the application's hosting model.
140
142
 
141
143
  Schedule `pruneReactions` per partition as a separate maintenance pass.
144
+ For commit-log pruning, expose the `SyncularRealtimeHost.pruneCommitLog` method
145
+ through the owning Durable Object and call its RPC from the scheduler. This
146
+ method uses the existing partition FIFO. Direct D1 pruning requires the same
147
+ external serialization assertion as push application.
142
148
  Defaults retain completed rows for 30 days and dead-lettered rows for 90 days,
143
149
  with at most 1,000 deletions per pass. Repeat while `mayHaveMore` is true.
144
150
  Pending and leased rows are never eligible. D1 executes each bounded cleanup
@@ -263,6 +269,9 @@ const realtimeDOConfig = (env: Env): RealtimeDOConfig => ({
263
269
  export class SyncularRealtimeDO extends DurableObject<Env> {
264
270
  #host = new SyncularRealtimeHost(this.ctx, this.env.DB, realtimeDOConfig(this.env));
265
271
  fetch(request: Request) { return this.#host.fetch(request); }
272
+ pruneCommitLog(partition: string, nowMs: number) {
273
+ return this.#host.pruneCommitLog({ partition, nowMs });
274
+ }
266
275
  webSocketMessage(ws: WebSocket, msg: ArrayBuffer | string) {
267
276
  return this.#host.webSocketMessage(ws, msg);
268
277
  }
@@ -67,7 +67,7 @@
67
67
  * partition serialization may call `/__wake` after its own commit so sockets
68
68
  * re-pull. See `durableObjectRealtimeNotifier` for that caller side.
69
69
  */
70
- import { D1ServerStorage, type RealtimeHubConfig } from '@syncular/server';
70
+ import { D1ServerStorage, type PruneOptions, type RealtimeHubConfig } from '@syncular/server';
71
71
  export interface DurableObjectStateLike {
72
72
  acceptWebSocket(ws: WebSocketLike, tags?: string[]): void;
73
73
  getWebSockets(tag?: string): WebSocketLike[];
@@ -153,6 +153,8 @@ export declare class SyncularRealtimeHost {
153
153
  * and accepts external-command wakes at `/__syncular_realtime/wake`.
154
154
  */
155
155
  fetch(request: Request): Promise<Response>;
156
+ /** Trusted maintenance shares the partition FIFO with HTTP/socket writes. */
157
+ pruneCommitLog(options: Omit<PruneOptions, 'storage'>): Promise<number>;
156
158
  /** Hibernation callback: an inbound frame. */
157
159
  webSocketMessage(ws: WebSocketLike, message: ArrayBuffer | string): Promise<void>;
158
160
  /** Hibernation callback: the socket closed. */
@@ -67,7 +67,7 @@
67
67
  * partition serialization may call `/__wake` after its own commit so sockets
68
68
  * re-pull. See `durableObjectRealtimeNotifier` for that caller side.
69
69
  */
70
- import { createRealtimeHub, D1ServerStorage, errorBody, handleSyncRequest, SSP2_CONTENT_TYPE, SyncError, } from '@syncular/server';
70
+ import { createRealtimeHub, D1ServerStorage, errorBody, handleSyncRequest, pruneCommitLog, SSP2_CONTENT_TYPE, SyncError, } from '@syncular/server';
71
71
  function isAttachment(value) {
72
72
  return (typeof value === 'object' &&
73
73
  value !== null &&
@@ -158,6 +158,13 @@ export class SyncularRealtimeHost {
158
158
  }
159
159
  return new Response('not found', { status: 404 });
160
160
  }
161
+ /** Trusted maintenance shares the partition FIFO with HTTP/socket writes. */
162
+ pruneCommitLog(options) {
163
+ return this.#serializePartition(async () => {
164
+ await this.#storage.migrate();
165
+ return pruneCommitLog({ ...options, storage: this.#storage });
166
+ });
167
+ }
161
168
  #serializePartition(operation) {
162
169
  const result = this.#partitionTail.then(operation, operation);
163
170
  this.#partitionTail = result.then(() => undefined, () => undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/server-workers",
3
- "version": "0.15.48",
3
+ "version": "0.17.0",
4
4
  "description": "Cloudflare Workers adapter for the Syncular sync server",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -45,10 +45,10 @@
45
45
  "!dist/**/*.test.d.ts"
46
46
  ],
47
47
  "dependencies": {
48
- "@syncular/server": "0.15.48",
49
- "@syncular/server-hono": "0.15.48"
48
+ "@syncular/server": "0.17.0",
49
+ "@syncular/server-hono": "0.17.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@syncular/core": "0.15.48"
52
+ "@syncular/core": "0.17.0"
53
53
  }
54
54
  }
@@ -72,6 +72,8 @@ import {
72
72
  D1ServerStorage,
73
73
  errorBody,
74
74
  handleSyncRequest,
75
+ pruneCommitLog,
76
+ type PruneOptions,
75
77
  type RealtimeHub,
76
78
  type RealtimeHubConfig,
77
79
  type RealtimeSession,
@@ -249,6 +251,14 @@ export class SyncularRealtimeHost {
249
251
  return new Response('not found', { status: 404 });
250
252
  }
251
253
 
254
+ /** Trusted maintenance shares the partition FIFO with HTTP/socket writes. */
255
+ pruneCommitLog(options: Omit<PruneOptions, 'storage'>): Promise<number> {
256
+ return this.#serializePartition(async () => {
257
+ await this.#storage.migrate();
258
+ return pruneCommitLog({ ...options, storage: this.#storage });
259
+ });
260
+ }
261
+
252
262
  #serializePartition<T>(operation: () => Promise<T>): Promise<T> {
253
263
  const result = this.#partitionTail.then(operation, operation);
254
264
  this.#partitionTail = result.then(