@syncular/server 0.15.25 → 0.15.27

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
@@ -57,6 +57,37 @@ done by a binding of the core or by in-database fanout — a relay would add a
57
57
  hop, a second protocol surface, and a managed dependency for zero capability
58
58
  the core lacks.
59
59
 
60
+ ## Startup schema readiness
61
+
62
+ Fail startup before accepting traffic when the generated schema cannot compile
63
+ or the storage projection cannot migrate:
64
+
65
+ ```ts
66
+ import {
67
+ ensureSyncServerReady,
68
+ SqliteServerStorage,
69
+ type SyncServerConfig,
70
+ } from '@syncular/server';
71
+
72
+ const config: SyncServerConfig = {
73
+ schema,
74
+ storage: new SqliteServerStorage('./sync.db'),
75
+ segments,
76
+ resolveScopes,
77
+ };
78
+
79
+ await ensureSyncServerReady(config);
80
+ Bun.serve({ fetch: app.fetch });
81
+ ```
82
+
83
+ The helper accepts the generated `ServerSchema`, compiles it, and calls the
84
+ storage backend's low-level `ensureSchema(CompiledSchema)`. A failure is a
85
+ `SyncServerReadinessError` with stable code `sync.schema_not_ready`, a `phase`
86
+ of `schema_compile` or `storage_migration`, and the generated schema version.
87
+ Log the cause for operators and stop startup. Do not catch readiness errors in
88
+ authentication or convert them to a 401; request-time schema checks are only a
89
+ defensive fallback.
90
+
60
91
  ## Write validators and recovery metadata
61
92
 
62
93
  `validators` is the server-authoritative seam for row business rules that
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export * from './postgres-storage.js';
26
26
  export * from './prune.js';
27
27
  export * from './pull.js';
28
28
  export * from './push.js';
29
+ export * from './readiness.js';
29
30
  export * from './realtime.js';
30
31
  export * from './relational-rows.js';
31
32
  export * from './s3-blob-store.js';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export * from './postgres-storage.js';
30
30
  export * from './prune.js';
31
31
  export * from './pull.js';
32
32
  export * from './push.js';
33
+ export * from './readiness.js';
33
34
  export * from './realtime.js';
34
35
  export * from './relational-rows.js';
35
36
  export * from './s3-blob-store.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Explicit server startup readiness.
3
+ *
4
+ * Protocol handlers ensure storage lazily for embeddability, but a production
5
+ * host must prove schema compatibility before it binds an HTTP/WebSocket port.
6
+ * This helper accepts the generated ServerSchema, compiles it once, and exposes
7
+ * a structured failure that cannot be confused with request authentication.
8
+ */
9
+ import type { SyncServerConfig } from './context.js';
10
+ export declare const SYNC_SERVER_READINESS_ERROR_CODE: "sync.schema_not_ready";
11
+ export type SyncServerReadinessPhase = 'schema_compile' | 'storage_migration';
12
+ export declare class SyncServerReadinessError extends Error {
13
+ readonly name = "SyncServerReadinessError";
14
+ readonly code: "sync.schema_not_ready";
15
+ readonly phase: SyncServerReadinessPhase;
16
+ readonly schemaVersion: number;
17
+ constructor(options: {
18
+ readonly phase: SyncServerReadinessPhase;
19
+ readonly schemaVersion: number;
20
+ readonly cause: unknown;
21
+ });
22
+ }
23
+ /**
24
+ * Compile and migrate the configured server storage before listening.
25
+ *
26
+ * Pass the same canonical config used by HTTP and realtime. The thrown error
27
+ * exposes only a stable code, phase, and schema version; operators can inspect
28
+ * `cause` locally for the table/column or storage diagnostic.
29
+ */
30
+ export declare function ensureSyncServerReady(config: Pick<SyncServerConfig, 'schema' | 'storage'>): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { compileSchema } from './schema.js';
2
+ export const SYNC_SERVER_READINESS_ERROR_CODE = 'sync.schema_not_ready';
3
+ export class SyncServerReadinessError extends Error {
4
+ name = 'SyncServerReadinessError';
5
+ code = SYNC_SERVER_READINESS_ERROR_CODE;
6
+ phase;
7
+ schemaVersion;
8
+ constructor(options) {
9
+ super(`Syncular server schema ${options.schemaVersion} is not ready (${options.phase})`, { cause: options.cause });
10
+ this.phase = options.phase;
11
+ this.schemaVersion = options.schemaVersion;
12
+ }
13
+ }
14
+ /**
15
+ * Compile and migrate the configured server storage before listening.
16
+ *
17
+ * Pass the same canonical config used by HTTP and realtime. The thrown error
18
+ * exposes only a stable code, phase, and schema version; operators can inspect
19
+ * `cause` locally for the table/column or storage diagnostic.
20
+ */
21
+ export async function ensureSyncServerReady(config) {
22
+ let compiled;
23
+ try {
24
+ compiled = compileSchema(config.schema);
25
+ }
26
+ catch (cause) {
27
+ throw new SyncServerReadinessError({
28
+ phase: 'schema_compile',
29
+ schemaVersion: config.schema.version,
30
+ cause,
31
+ });
32
+ }
33
+ try {
34
+ await config.storage.ensureSchema(compiled);
35
+ }
36
+ catch (cause) {
37
+ throw new SyncServerReadinessError({
38
+ phase: 'storage_migration',
39
+ schemaVersion: config.schema.version,
40
+ cause,
41
+ });
42
+ }
43
+ }
package/dist/storage.d.ts CHANGED
@@ -237,6 +237,11 @@ export interface ServerStorage {
237
237
  * INDEX). The handler calls this before serving; hosts that drive
238
238
  * storage directly (tests, admin tooling) call it once up front. Row
239
239
  * operations for tables not covered by an `ensureSchema` call throw.
240
+ *
241
+ * This low-level storage seam accepts a `CompiledSchema`. Application hosts
242
+ * should call `ensureSyncServerReady(config)` with their generated schema
243
+ * before binding a public port; protocol handlers still call this method
244
+ * lazily as a defensive backstop.
240
245
  */
241
246
  ensureSchema(schema: CompiledSchema): Promise<void>;
242
247
  begin(partition: string): Promise<StorageTransaction>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/server",
3
- "version": "0.15.25",
3
+ "version": "0.15.27",
4
4
  "description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -53,7 +53,7 @@
53
53
  "!dist/**/*.test.d.ts"
54
54
  ],
55
55
  "dependencies": {
56
- "@syncular/core": "0.15.25"
56
+ "@syncular/core": "0.15.27"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@electric-sql/pglite": "^0.5.4"
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export * from './postgres-storage';
30
30
  export * from './prune';
31
31
  export * from './pull';
32
32
  export * from './push';
33
+ export * from './readiness';
33
34
  export * from './realtime';
34
35
  export * from './relational-rows';
35
36
  export * from './s3-blob-store';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Explicit server startup readiness.
3
+ *
4
+ * Protocol handlers ensure storage lazily for embeddability, but a production
5
+ * host must prove schema compatibility before it binds an HTTP/WebSocket port.
6
+ * This helper accepts the generated ServerSchema, compiles it once, and exposes
7
+ * a structured failure that cannot be confused with request authentication.
8
+ */
9
+ import type { SyncServerConfig } from './context';
10
+ import { type CompiledSchema, compileSchema } from './schema';
11
+
12
+ export const SYNC_SERVER_READINESS_ERROR_CODE =
13
+ 'sync.schema_not_ready' as const;
14
+
15
+ export type SyncServerReadinessPhase = 'schema_compile' | 'storage_migration';
16
+
17
+ export class SyncServerReadinessError extends Error {
18
+ override readonly name = 'SyncServerReadinessError';
19
+ readonly code = SYNC_SERVER_READINESS_ERROR_CODE;
20
+ readonly phase: SyncServerReadinessPhase;
21
+ readonly schemaVersion: number;
22
+
23
+ constructor(options: {
24
+ readonly phase: SyncServerReadinessPhase;
25
+ readonly schemaVersion: number;
26
+ readonly cause: unknown;
27
+ }) {
28
+ super(
29
+ `Syncular server schema ${options.schemaVersion} is not ready (${options.phase})`,
30
+ { cause: options.cause },
31
+ );
32
+ this.phase = options.phase;
33
+ this.schemaVersion = options.schemaVersion;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Compile and migrate the configured server storage before listening.
39
+ *
40
+ * Pass the same canonical config used by HTTP and realtime. The thrown error
41
+ * exposes only a stable code, phase, and schema version; operators can inspect
42
+ * `cause` locally for the table/column or storage diagnostic.
43
+ */
44
+ export async function ensureSyncServerReady(
45
+ config: Pick<SyncServerConfig, 'schema' | 'storage'>,
46
+ ): Promise<void> {
47
+ let compiled: CompiledSchema;
48
+ try {
49
+ compiled = compileSchema(config.schema);
50
+ } catch (cause) {
51
+ throw new SyncServerReadinessError({
52
+ phase: 'schema_compile',
53
+ schemaVersion: config.schema.version,
54
+ cause,
55
+ });
56
+ }
57
+ try {
58
+ await config.storage.ensureSchema(compiled);
59
+ } catch (cause) {
60
+ throw new SyncServerReadinessError({
61
+ phase: 'storage_migration',
62
+ schemaVersion: config.schema.version,
63
+ cause,
64
+ });
65
+ }
66
+ }
package/src/storage.ts CHANGED
@@ -269,6 +269,11 @@ export interface ServerStorage {
269
269
  * INDEX). The handler calls this before serving; hosts that drive
270
270
  * storage directly (tests, admin tooling) call it once up front. Row
271
271
  * operations for tables not covered by an `ensureSchema` call throw.
272
+ *
273
+ * This low-level storage seam accepts a `CompiledSchema`. Application hosts
274
+ * should call `ensureSyncServerReady(config)` with their generated schema
275
+ * before binding a public port; protocol handlers still call this method
276
+ * lazily as a defensive backstop.
272
277
  */
273
278
  ensureSchema(schema: CompiledSchema): Promise<void>;
274
279