orez-sync-cf-host 0.4.52

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 ADDED
@@ -0,0 +1,143 @@
1
+ # `sync-cf-host`
2
+
3
+ This is the production-shaped Cloudflare Durable Object host for the Rust sync
4
+ engine. The package exposes a consumer integration surface (`createSyncWorker`,
5
+ `createSyncDurableObject`, `registerMutators`) and a harness deployment built
6
+ from `src/harness-worker.ts`. Consumers provide a JSON Zero schema, application
7
+ DDL/seed initializer, normalized-claims authenticator, mutator registry, and an
8
+ optional row-visibility hook.
9
+
10
+ The Worker authenticates at the consumer edge, forwards only normalized claims
11
+ to a per-namespace Durable Object, and never logs tokens, mutation arguments, or
12
+ row contents. Pull runs `engine_handle_pull` inside `transactionSync`; push runs
13
+ Rust `push_validate`/`preflight`/`finalize`/`record_app_error` steps around the
14
+ registered asynchronous TypeScript mutator inside `ctx.storage.transaction`.
15
+ Every SQL cursor is materialized before an await. Application effects are
16
+ collected and run only after their transaction commits; application failures use
17
+ the required second transaction to advance the LMID marker.
18
+
19
+ ## Wake channel and eviction
20
+
21
+ `GET /<namespace>/wake?clientID=<id>` upgrades to a Durable Object hibernating
22
+ WebSocket. Socket attachments carry only the client ID. A committed push sends a
23
+ text `wake` frame to all connected clients except the pusher; a scheduler window
24
+ coalesces a burst into one frame per socket. `ping` receives `pong`. The message
25
+ contains no state and carries no correctness weight: clients pull after a wake
26
+ and retain their safety poll. `ctx.getWebSockets()` plus serialized attachments
27
+ means sockets remain discoverable after hibernation/re-instantiation.
28
+
29
+ `/admin/status` reports a boot ID, hibernation simulation count, connected wake
30
+ sockets, durable database size, engine watermark/floor, and aggregate counters.
31
+ After the configured idle gap (5 seconds in the harness deployment), the local
32
+ deterministic model resets in-memory state and changes boot ID while retaining
33
+ SQLite state and sockets. This models the harness/cf idle-teardown pattern; it
34
+ does not claim that a real platform eviction happens on a 5-second schedule.
35
+
36
+ ## Authenticated operator controls
37
+
38
+ All `/admin/*` routes are rejected unless the consumer's `authorizeAdmin`
39
+ callback succeeds or the request presents the deployment's `ADMIN_KEY` through
40
+ `x-admin-key`. Harness and operator deployments expose `/admin/status`; it
41
+ includes the persisted writer-enabled state and wasm linear-memory byte length
42
+ in addition to the engine/counter fields above.
43
+
44
+ `POST /admin/writer` with `{ "enabled": false }` durably stops pushes for that
45
+ namespace. A stopped writer consumes and discards the request body, returns 503,
46
+ and performs no engine or application write. Canary rollback drills must stop
47
+ one writer and prove rejection before enabling another. The test-only runner is:
48
+
49
+ ```sh
50
+ mise exec node@24.3.0 -- bun harness/src/rollback-drill.ts --confirm-test-only
51
+ ```
52
+
53
+ The runner accepts only loopback or the `lslcf.workers.dev` test worker and does
54
+ not mutate a production route. These controls are mechanisms, not authorization
55
+ to perform a production cutover.
56
+
57
+ ## Counter and HTTP wire representation
58
+
59
+ Inside the wasm/JavaScript engine boundary, cookies, watermarks, and LMIDs are
60
+ canonical decimal strings (`0` or non-zero ASCII digits), parsed as signed
61
+ SQLite-range `i64`; exact SQLite reads use `CAST(... AS TEXT)`. The baseline HTTP
62
+ wire remains JSON numbers for Zero 1.7 byte compatibility, accepted only within
63
+ the JavaScript safe-integer range. M1 centralizes this conversion in
64
+ `sync-core::wire::counter_to_json` and accepts either representation inbound.
65
+ No engine code silently converts an unsafe persisted counter through a JS
66
+ `Number`.
67
+
68
+ ## Local checks
69
+
70
+ ```sh
71
+ cd packages/sync-cf-host
72
+ bun install
73
+ bun run typecheck # builds probe-enabled wasm for all TS declarations
74
+ bun run test:platform # M0 boundary regression: 36 local workerd assertions
75
+ bun run test:integration # production host: pull/push/rollback/wake/eviction
76
+ bun run bundle # Wrangler dry-run bundle report
77
+ bun run measure # cold start/CPU/storage/memory baseline
78
+ cargo test -p sync-core # engine/model/reference/soot suites (from repo root)
79
+ ```
80
+
81
+ The production integration suite can target a deployed Worker:
82
+
83
+ ```sh
84
+ M3_BASE_URL=https://orez-rust-sync.lslcf.workers.dev \
85
+ M3_ADMIN_KEY="$(tr -d '\n' < ~/.zharness-cf-admin-key)" \
86
+ bun integration-test.mjs
87
+ ```
88
+
89
+ The platform regression remains a separate feature-enabled test path so probe
90
+ helpers are absent from the production wasm bundle:
91
+
92
+ ```sh
93
+ bun run test:platform
94
+ ```
95
+
96
+ ## M3 measurements
97
+
98
+ Measured 2026-07-10 UTC on darwin-arm64, Bun 1.3.10, Rust 1.94.0,
99
+ wasm-pack 0.14.0, Wrangler 4.103.0, workerd 1.20260617.1. Local workerd was
100
+ started with the production `orez-rust-sync` config and an equivalent local
101
+ admin variable; deployed requests used the `lslcf` account and returned a
102
+ Cloudflare LAX edge (`cf-ray …-LAX`).
103
+
104
+ | Measurement | Result |
105
+ | -------------------------------------------------------------------- | --------------------------------------------------------------------- |
106
+ | Local production integration | 16 assertions passed |
107
+ | Deployed production integration | 16 assertions passed |
108
+ | Local M0 platform regression | 36 assertions passed |
109
+ | Rust core tests | 27 reference + 13 composition + 2 model tests passed |
110
+ | Bundle upload | 328.34 KiB total; 130.45 KiB gzip |
111
+ | Wasm module | approximately 273 KiB on disk |
112
+ | Wrangler reported startup | 1 ms |
113
+ | Local cold DO pull (30 namespaces) | p50 5.362 ms; p95 7.081 ms |
114
+ | Local push acknowledgement (50 mutations) | p50 1.507 ms; p95 2.971 ms |
115
+ | Seeded storage | 81,920 bytes |
116
+ | Storage after 50 pushes | 90,112 bytes (+8,192 bytes) |
117
+ | Local workerd RSS | 97.391 MiB baseline; 146.625 MiB after load (+49.234 MiB process RSS) |
118
+ | CF eviction lane | boot ID changed; 20 writes; 126 pulls; zero 409s; monotone cookies |
119
+ | CF wake-only storm (100 clients, 5 writers, 10 s safety poll) | propagation p50/p95 809/810 ms |
120
+ | CF clean-write propagation (10 clients, 20 writes, 10 s safety poll) | commit-to-seen p50/p95 136/406 ms; issue-to-seen p95 858 ms |
121
+ | CF 10-client/2-writer bench | ack p50/p95 178/243 ms; propagation p50/p95 381/524 ms |
122
+ | Equivalent TS DO bench | ack p50/p95 165/903 ms; propagation p50/p95 583/1,074 ms |
123
+
124
+ Wake fan-out is anchored with `waitUntil` after commit and is not on the push
125
+ response's critical path. The acknowledgement result is a local wall-time
126
+ proxy, not billed Cloudflare CPU. RSS is the whole local
127
+ workerd process, not an isolate allocation; Cloudflare's 128 MiB per-isolate
128
+ limit cannot be inferred from that process number. The bundle is 95.8% below
129
+ the 3 MiB gzip Free-plan limit (and 98.7% below the 10 MiB paid-plan limit).
130
+ See [Cloudflare Worker limits](https://developers.cloudflare.com/workers/platform/limits/).
131
+
132
+ ## Deployment
133
+
134
+ `wrangler.toml` is checked in with Worker name `orez-rust-sync`, SQLite Durable
135
+ Object class `SyncDurableObject`, and account `6afff1f79e2fd12f1cfd1bfe1dfd08d1`.
136
+ The deployed test version used for this M3 pass was
137
+ `871a13df-7c3b-4e1c-bc90-c3cbd00f2dea` at
138
+ `https://orez-rust-sync.lslcf.workers.dev`. The throwaway M0 Worker was already
139
+ absent when the cleanup command was run (Cloudflare returned error 10090), so no
140
+ old probe service remains to receive traffic.
141
+
142
+ The rust toolchain is pinned at the workspace root in
143
+ [rust-toolchain.toml](/Users/n8/.worktrees/orez-rust-sync/rust-toolchain.toml).
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "orez-sync-cf-host",
3
+ "version": "0.4.52",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "scripts": {
9
+ "build:wasm": "wasm-pack build ../../crates/sync-wasm --target web --release --out-dir ../../packages/sync-cf-host/src/generated --out-name sync_wasm && rm -f src/generated/.gitignore && bun scripts/write-wasm-module-type.ts",
10
+ "build:wasm:platform": "wasm-pack build ../../crates/sync-wasm --target web --release --out-dir ../../packages/sync-cf-host/src/generated --out-name sync_wasm -- --features platform-probes && rm -f src/generated/.gitignore && bun scripts/write-wasm-module-type.ts",
11
+ "bundle": "bun run build:wasm && wrangler deploy --dry-run --outdir dist",
12
+ "measure": "bun run build:wasm && bun measure.mjs",
13
+ "test": "bun run test:config && bun run test:platform && bun run test:integration && bun run test:ingest && bun run test:restart",
14
+ "test:config": "bun test config.test.mjs",
15
+ "test:integration": "bun run build:wasm && bun integration-test.mjs",
16
+ "test:ingest": "bun run build:wasm && bun ingest-test.mjs",
17
+ "test:restart": "bun restart-test.mjs",
18
+ "test:platform": "bun run build:wasm:platform && bun platform-test.mjs",
19
+ "typecheck": "bun run build:wasm:platform && tsc --noEmit"
20
+ },
21
+ "devDependencies": {
22
+ "@cloudflare/workers-types": "4.20260617.1",
23
+ "typescript": "5.9.3",
24
+ "workerd": "1.20260617.1",
25
+ "wrangler": "4.103.0"
26
+ },
27
+ "files": [
28
+ "src"
29
+ ]
30
+ }
package/src/config.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { PullCaps, SyncHostConfig, SyncHostEnv } from './types.js'
2
+
3
+ export function validatePullCaps(caps: PullCaps): PullCaps {
4
+ if (!Number.isSafeInteger(caps.maxChangeRows) || caps.maxChangeRows < 1) {
5
+ throw new TypeError('caps.maxChangeRows must be a positive safe integer')
6
+ }
7
+ return caps
8
+ }
9
+
10
+ export function validateSyncHostConfig<Env extends SyncHostEnv>(
11
+ config: SyncHostConfig<Env>
12
+ ): SyncHostConfig<Env> {
13
+ const hasMutators = config.mutators !== undefined
14
+ const hasDelegate = config.mutateUrl !== undefined
15
+ if (hasMutators === hasDelegate) {
16
+ throw new TypeError('sync host config requires exactly one of mutators or mutateUrl')
17
+ }
18
+ if (hasDelegate && !config.upstream) {
19
+ throw new TypeError('sync host config mutateUrl requires upstream')
20
+ }
21
+ if (config.mutateBinding !== undefined && !hasDelegate) {
22
+ throw new TypeError('sync host config mutateBinding requires mutateUrl')
23
+ }
24
+ if (config.mutateBinding !== undefined && !config.mutateBinding) {
25
+ throw new TypeError('sync host config mutateBinding must not be empty')
26
+ }
27
+ if (hasMutators && config.upstream) {
28
+ throw new TypeError(
29
+ 'sync host config cannot combine local mutators with upstream ingest'
30
+ )
31
+ }
32
+ if (config.mutateUrl && !config.mutateUrl.startsWith('/')) {
33
+ throw new TypeError('mutateUrl must be an absolute path')
34
+ }
35
+ if (config.upstream) {
36
+ if (!config.upstream.binding) throw new TypeError('upstream.binding is required')
37
+ const limit = config.upstream.changeLimit ?? 1_000
38
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000) {
39
+ throw new TypeError('upstream.changeLimit must be a safe integer in 1..10000')
40
+ }
41
+ const interval = config.upstream.intervalMs ?? 15_000
42
+ if (!Number.isSafeInteger(interval) || interval < 1_000) {
43
+ throw new TypeError('upstream.intervalMs must be a safe integer >= 1000')
44
+ }
45
+ }
46
+ return config
47
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "sync-wasm",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "license": "MIT",
6
+ "files": [
7
+ "sync_wasm_bg.wasm",
8
+ "sync_wasm.js",
9
+ "sync_wasm.d.ts"
10
+ ],
11
+ "main": "sync_wasm.js",
12
+ "types": "sync_wasm.d.ts",
13
+ "sideEffects": [
14
+ "./snippets/*"
15
+ ]
16
+ }
@@ -0,0 +1,123 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Apply one ordered page from the upstream ZeroSqlDO change feed. The host
6
+ * owns one transaction around this call; application triggers append the
7
+ * ordinary engine change-log entries consumed by pull/CVR code.
8
+ */
9
+ export function engine_apply_upstream(db: any, schema: any, batch: any): any;
10
+
11
+ /**
12
+ * Atomically rebuild application rows from a point-in-time upstream snapshot.
13
+ */
14
+ export function engine_apply_upstream_snapshot(db: any, schema: any, snapshot: any): any;
15
+
16
+ export function engine_assemble_push_response(results: any): any;
17
+
18
+ /**
19
+ * Compile a validated Zero query AST for a consumer mutator's transactional
20
+ * `tx.run(...)`. Execution remains in the host-owned application transaction.
21
+ */
22
+ export function engine_compile_query(schema: any, ast: any): any;
23
+
24
+ export function engine_finalize(db: any, client_group_id: string, client_id: string, mutation_id: string): void;
25
+
26
+ /**
27
+ * Production pull entry. The TypeScript host owns `transactionSync`.
28
+ */
29
+ export function engine_handle_pull(db: any, schema: any, visibility: any, caps: any, retain_changes: string, body: any, user_id: string): any;
30
+
31
+ /**
32
+ * Query-aware pull entry point. Desired-query ASTs are already resolved and
33
+ * validated by the consumer host before crossing this boundary.
34
+ */
35
+ export function engine_handle_query_pull(db: any, schema: any, retain_changes: string, body: any, user_id: string): any;
36
+
37
+ /**
38
+ * Initialize the additive query-aware durable tables. The host owns the
39
+ * transaction boundary, exactly as it does for the baseline schema.
40
+ */
41
+ export function engine_init_query_schema(db: any): void;
42
+
43
+ /**
44
+ * Initialize the sync engine's durable metadata and triggers. The host calls
45
+ * this inside startup `transactionSync` after application DDL and seed.
46
+ */
47
+ export function engine_init_schema(db: any, schema: any): void;
48
+
49
+ export function engine_invalidate(db: any): void;
50
+
51
+ /**
52
+ * Linear-memory size for authenticated operator soak diagnostics. This is a
53
+ * byte count only; it contains no application or query data.
54
+ */
55
+ export function engine_memory_bytes(): number;
56
+
57
+ export function engine_preflight(db: any, client_group_id: string, client_id: string, mutation_id: string, user_id: string): any;
58
+
59
+ export function engine_prune(db: any, retain_changes: string): void;
60
+
61
+ /**
62
+ * Validate an entire push before the host opens the first mutation tx.
63
+ */
64
+ export function engine_push_validate(body: any): any;
65
+
66
+ export function engine_record_app_error(db: any, client_group_id: string, client_id: string, mutation_id: string, user_id: string): void;
67
+
68
+ export function engine_state(db: any): any;
69
+
70
+ export function engine_version(): string;
71
+
72
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
73
+
74
+ export interface InitOutput {
75
+ readonly memory: WebAssembly.Memory;
76
+ readonly engine_apply_upstream: (a: any, b: any, c: any) => [number, number, number];
77
+ readonly engine_apply_upstream_snapshot: (a: any, b: any, c: any) => [number, number, number];
78
+ readonly engine_assemble_push_response: (a: any) => [number, number, number];
79
+ readonly engine_compile_query: (a: any, b: any) => [number, number, number];
80
+ readonly engine_finalize: (a: any, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
81
+ readonly engine_handle_pull: (a: any, b: any, c: any, d: any, e: number, f: number, g: any, h: number, i: number) => [number, number, number];
82
+ readonly engine_handle_query_pull: (a: any, b: any, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
83
+ readonly engine_init_query_schema: (a: any) => [number, number];
84
+ readonly engine_init_schema: (a: any, b: any) => [number, number];
85
+ readonly engine_invalidate: (a: any) => [number, number];
86
+ readonly engine_memory_bytes: () => [number, number, number];
87
+ readonly engine_preflight: (a: any, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number];
88
+ readonly engine_prune: (a: any, b: number, c: number) => [number, number];
89
+ readonly engine_push_validate: (a: any) => [number, number, number];
90
+ readonly engine_record_app_error: (a: any, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number];
91
+ readonly engine_state: (a: any) => [number, number, number];
92
+ readonly engine_version: () => [number, number];
93
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
94
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
95
+ readonly __wbindgen_exn_store: (a: number) => void;
96
+ readonly __externref_table_alloc: () => number;
97
+ readonly __wbindgen_externrefs: WebAssembly.Table;
98
+ readonly __externref_table_dealloc: (a: number) => void;
99
+ readonly __wbindgen_free: (a: number, b: number, c: number) => void;
100
+ readonly __wbindgen_start: () => void;
101
+ }
102
+
103
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
104
+
105
+ /**
106
+ * Instantiates the given `module`, which can either be bytes or
107
+ * a precompiled `WebAssembly.Module`.
108
+ *
109
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
110
+ *
111
+ * @returns {InitOutput}
112
+ */
113
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
114
+
115
+ /**
116
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
117
+ * for everything else, calls `WebAssembly.instantiate` directly.
118
+ *
119
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
120
+ *
121
+ * @returns {Promise<InitOutput>}
122
+ */
123
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;