orez-sync-cf-host 0.5.19 → 0.5.21

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 (52) hide show
  1. package/README.md +49 -6
  2. package/dist/bun-wasm-loader.d.ts +1 -0
  3. package/dist/bun-wasm-loader.js +20 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +93 -0
  6. package/{src → dist}/generated/sync_wasm_bg.wasm +0 -0
  7. package/dist/host.d.ts +12 -0
  8. package/dist/host.js +1512 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.js +4 -0
  11. package/dist/mutation-error.d.ts +9 -0
  12. package/dist/mutation-error.js +23 -0
  13. package/dist/node-wasm-loader.d.ts +1 -0
  14. package/dist/node-wasm-loader.js +22 -0
  15. package/dist/post-commit.d.ts +13 -0
  16. package/dist/post-commit.js +31 -0
  17. package/dist/query-compiler.d.ts +4 -0
  18. package/dist/query-compiler.js +4 -0
  19. package/dist/sql-storage-adapter.d.ts +69 -0
  20. package/dist/sql-storage-adapter.js +194 -0
  21. package/dist/transaction-query.d.ts +72 -0
  22. package/dist/transaction-query.js +379 -0
  23. package/dist/types.d.ts +159 -0
  24. package/dist/types.js +5 -0
  25. package/dist/vite-wasm-loader.d.ts +3 -0
  26. package/dist/vite-wasm-loader.js +24 -0
  27. package/dist/wasm.d.ts +1 -0
  28. package/dist/wasm.js +7 -0
  29. package/dist/write-safeguards.d.ts +53 -0
  30. package/dist/write-safeguards.js +186 -0
  31. package/package.json +59 -19
  32. package/src/bun-wasm-loader.ts +0 -16
  33. package/src/config.ts +0 -102
  34. package/src/harness-config.ts +0 -487
  35. package/src/harness-worker.ts +0 -33
  36. package/src/host.ts +0 -1958
  37. package/src/index.ts +0 -47
  38. package/src/ingest-harness-worker.ts +0 -535
  39. package/src/mutation-error.ts +0 -28
  40. package/src/platform-probe-worker.ts +0 -418
  41. package/src/post-commit.ts +0 -40
  42. package/src/query-compiler.ts +0 -16
  43. package/src/sql-storage-adapter.ts +0 -254
  44. package/src/transaction-query.ts +0 -494
  45. package/src/types.ts +0 -195
  46. package/src/wasm-platform.ts +0 -10
  47. package/src/wasm.ts +0 -34
  48. package/src/write-safeguards.ts +0 -248
  49. /package/{src → dist}/generated/package.json +0 -0
  50. /package/{src → dist}/generated/sync_wasm.d.ts +0 -0
  51. /package/{src → dist}/generated/sync_wasm.js +0 -0
  52. /package/{src → dist}/generated/sync_wasm_bg.wasm.d.ts +0 -0
@@ -0,0 +1,186 @@
1
+ export class IngestBreakerError extends Error {
2
+ error;
3
+ windowRows;
4
+ budget;
5
+ retryAfterMs;
6
+ status = 429;
7
+ constructor(error, windowRows, budget, retryAfterMs) {
8
+ super(`${error}: ${windowRows}/${budget} rows; retry after ${retryAfterMs}ms`);
9
+ this.error = error;
10
+ this.windowRows = windowRows;
11
+ this.budget = budget;
12
+ this.retryAfterMs = retryAfterMs;
13
+ this.name = 'IngestBreakerError';
14
+ }
15
+ }
16
+ export class IngestCircuitBreaker {
17
+ #options;
18
+ #samples = [];
19
+ #billableRows = 0;
20
+ #logicalRows = 0;
21
+ #reason = null;
22
+ #retryAt = null;
23
+ #consecutiveTrips = 0;
24
+ constructor(options) {
25
+ for (const [name, value] of Object.entries(options)) {
26
+ if (name === 'now')
27
+ continue;
28
+ if (!Number.isSafeInteger(value) || Number(value) < 1)
29
+ throw new TypeError(`${name} must be a positive safe integer`);
30
+ }
31
+ this.#options = options;
32
+ }
33
+ #prune(now) {
34
+ const cutoff = now - this.#options.windowMs;
35
+ let remove = 0;
36
+ while (remove < this.#samples.length && this.#samples[remove].at <= cutoff) {
37
+ this.#billableRows -= this.#samples[remove].billableRows;
38
+ this.#logicalRows -= this.#samples[remove].logicalRows;
39
+ remove++;
40
+ }
41
+ if (remove > 0)
42
+ this.#samples.splice(0, remove);
43
+ }
44
+ status() {
45
+ const now = this.#options.now();
46
+ this.#prune(now);
47
+ const retryAfterMs = Math.max(0, (this.#retryAt ?? now) - now);
48
+ return {
49
+ windowRows: this.#billableRows,
50
+ billableRows: this.#billableRows,
51
+ logicalRows: this.#logicalRows,
52
+ budget: this.#options.budgetRows,
53
+ windowMs: this.#options.windowMs,
54
+ tripped: this.#reason !== null && retryAfterMs > 0,
55
+ reason: this.#reason,
56
+ retryAt: this.#retryAt,
57
+ retryAfterMs,
58
+ consecutiveTrips: this.#consecutiveTrips,
59
+ };
60
+ }
61
+ assertReady() {
62
+ const status = this.status();
63
+ if (status.tripped && status.reason) {
64
+ throw new IngestBreakerError(status.reason, status.windowRows, status.budget, status.retryAfterMs);
65
+ }
66
+ }
67
+ record(rows) {
68
+ this.recordLogical(rows);
69
+ this.recordBillable(rows);
70
+ }
71
+ #sample(now) {
72
+ this.#prune(now);
73
+ const last = this.#samples[this.#samples.length - 1];
74
+ if (last?.at === now)
75
+ return last;
76
+ const sample = { at: now, billableRows: 0, logicalRows: 0 };
77
+ this.#samples.push(sample);
78
+ return sample;
79
+ }
80
+ recordLogical(rows) {
81
+ if (!Number.isSafeInteger(rows) || rows <= 0)
82
+ return;
83
+ const sample = this.#sample(this.#options.now());
84
+ sample.logicalRows += rows;
85
+ this.#logicalRows += rows;
86
+ }
87
+ recordBillable(rows) {
88
+ this.assertReady();
89
+ if (!Number.isSafeInteger(rows) || rows <= 0)
90
+ return;
91
+ const now = this.#options.now();
92
+ const sample = this.#sample(now);
93
+ sample.billableRows += rows;
94
+ this.#billableRows += rows;
95
+ if (this.#billableRows > this.#options.budgetRows)
96
+ this.trip('ingestBudgetExceeded');
97
+ }
98
+ trip(reason) {
99
+ const now = this.#options.now();
100
+ this.#consecutiveTrips++;
101
+ const delay = Math.min(this.#options.maxBackoffMs, this.#options.initialBackoffMs * 2 ** (this.#consecutiveTrips - 1));
102
+ this.#reason = reason;
103
+ this.#retryAt = now + delay;
104
+ throw new IngestBreakerError(reason, this.#billableRows, this.#options.budgetRows, delay);
105
+ }
106
+ recovered() {
107
+ this.#reason = null;
108
+ this.#retryAt = null;
109
+ this.#consecutiveTrips = 0;
110
+ }
111
+ restore(reason, retryAt, consecutiveTrips) {
112
+ if (!Number.isFinite(retryAt) || retryAt <= 0)
113
+ return;
114
+ this.#reason = reason;
115
+ this.#retryAt = retryAt;
116
+ this.#consecutiveTrips = Math.max(1, Math.floor(consecutiveTrips) || 1);
117
+ }
118
+ reopen() {
119
+ this.#samples = [];
120
+ this.#billableRows = 0;
121
+ this.#logicalRows = 0;
122
+ this.recovered();
123
+ }
124
+ }
125
+ export function trackBillableCursorRows(cursor, record) {
126
+ if (!cursor || typeof cursor !== 'object')
127
+ return cursor;
128
+ let accountedRows = 0;
129
+ const account = () => {
130
+ const current = Number(cursor.rowsWritten ?? 0);
131
+ if (!Number.isSafeInteger(current) || current <= accountedRows)
132
+ return;
133
+ const delta = current - accountedRows;
134
+ accountedRows = current;
135
+ record(delta);
136
+ };
137
+ const wrapIterator = (iterator) => {
138
+ const next = iterator.next.bind(iterator);
139
+ return {
140
+ next(...args) {
141
+ try {
142
+ return next(...args);
143
+ }
144
+ finally {
145
+ account();
146
+ }
147
+ },
148
+ [Symbol.iterator]() {
149
+ return this;
150
+ },
151
+ };
152
+ };
153
+ let proxy;
154
+ proxy = new Proxy(cursor, {
155
+ get(target, property) {
156
+ const value = Reflect.get(target, property, target);
157
+ if (property === Symbol.iterator)
158
+ return () => proxy;
159
+ if ((property === 'next' || property === 'one' || property === 'toArray') &&
160
+ typeof value === 'function') {
161
+ return (...args) => {
162
+ try {
163
+ return Reflect.apply(value, target, args);
164
+ }
165
+ finally {
166
+ account();
167
+ }
168
+ };
169
+ }
170
+ if (property === 'raw' && typeof value === 'function') {
171
+ return (...args) => wrapIterator(Reflect.apply(value, target, args));
172
+ }
173
+ return typeof value === 'function' ? value.bind(target) : value;
174
+ },
175
+ });
176
+ account();
177
+ return proxy;
178
+ }
179
+ export function retryDelayMs(attempt, initialBackoffMs, maxBackoffMs) {
180
+ return Math.min(maxBackoffMs, initialBackoffMs * 2 ** Math.max(0, attempt - 1));
181
+ }
182
+ export function shouldRetryDelegatedPush(responseStatus, attempt, maxAttempts) {
183
+ if (attempt >= maxAttempts)
184
+ return false;
185
+ return responseStatus === null || responseStatus === 429 || responseStatus >= 500;
186
+ }
package/package.json CHANGED
@@ -1,31 +1,62 @@
1
1
  {
2
2
  "name": "orez-sync-cf-host",
3
- "version": "0.5.19",
3
+ "version": "0.5.21",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": "./src/index.ts",
7
- "./post-commit": "./src/post-commit.ts",
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ },
10
+ "./post-commit": {
11
+ "types": "./dist/post-commit.d.ts",
12
+ "import": "./dist/post-commit.js"
13
+ },
8
14
  "./mutation-error": {
9
- "types": "./src/mutation-error.ts",
10
- "import": "./src/mutation-error.ts"
15
+ "types": "./dist/mutation-error.d.ts",
16
+ "import": "./dist/mutation-error.js"
17
+ },
18
+ "./node-wasm-loader": {
19
+ "types": "./dist/node-wasm-loader.d.ts",
20
+ "import": "./dist/node-wasm-loader.js"
21
+ },
22
+ "./bun-wasm-loader": {
23
+ "types": "./dist/bun-wasm-loader.d.ts",
24
+ "import": "./dist/bun-wasm-loader.js"
25
+ },
26
+ "./query-compiler": {
27
+ "types": "./dist/query-compiler.d.ts",
28
+ "import": "./dist/query-compiler.js"
11
29
  },
12
- "./bun-wasm-loader": "./src/bun-wasm-loader.ts",
13
- "./query-compiler": "./src/query-compiler.ts",
14
- "./transaction-query": "./src/transaction-query.ts"
30
+ "./transaction-query": {
31
+ "types": "./dist/transaction-query.d.ts",
32
+ "import": "./dist/transaction-query.js"
33
+ },
34
+ "./vite-wasm-loader": {
35
+ "types": "./dist/vite-wasm-loader.d.ts",
36
+ "import": "./dist/vite-wasm-loader.js"
37
+ },
38
+ "./wasm-module.wasm": {
39
+ "types": "./dist/generated/sync_wasm_bg.wasm.d.ts",
40
+ "import": "./dist/generated/sync_wasm_bg.wasm"
41
+ }
15
42
  },
16
43
  "scripts": {
17
44
  "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",
18
45
  "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",
19
- "bundle": "bun run build:wasm && wrangler deploy --dry-run --outdir dist",
20
- "measure": "bun run build:wasm && bun measure.mjs",
21
- "test": "bun run test:config && bun run test:query-compiler && bun run test:platform && bun run test:integration && bun run test:ingest && bun run test:restart",
22
- "test:config": "bun test config.test.mjs write-safeguards.test.mjs mutation-error.test.mjs post-commit.test.mjs",
23
- "test:integration": "bun run build:wasm && bun integration-test.mjs",
24
- "test:ingest": "bun run build:wasm && bun ingest-test.mjs",
25
- "test:query-compiler": "bun run build:wasm && bun test --preload orez-sync-cf-host/bun-wasm-loader query-compiler.test.mjs",
26
- "test:restart": "bun restart-test.mjs",
27
- "test:platform": "bun run build:wasm:platform && bun platform-test.mjs",
28
- "typecheck": "bun run build:wasm:platform && tsc --noEmit"
46
+ "build:runtime": "rm -rf dist && mkdir -p dist && cp -R src/generated dist/generated && tsc -p tsconfig.build.json",
47
+ "build:dist": "bun run build:wasm && bun run build:runtime",
48
+ "build:dist:platform": "bun run build:wasm:platform && bun run build:runtime",
49
+ "bundle": "bun run build:dist && wrangler deploy --dry-run --outdir .wrangler/bundle",
50
+ "measure": "bun run build:dist && bun measure.mjs",
51
+ "test": "bun run test:config && bun run test:query-compiler && bun run test:package && bun run test:platform && bun run test:integration && bun run test:ingest && bun run test:restart",
52
+ "test:config": "bun test config.test.mjs write-safeguards.test.mjs mutation-error.test.mjs post-commit.test.mjs transaction-query.test.mjs",
53
+ "test:integration": "bun run build:dist && bun integration-test.mjs",
54
+ "test:ingest": "bun run build:dist && bun ingest-test.mjs",
55
+ "test:package": "bun run build:dist && node package-runtime-test.mjs",
56
+ "test:query-compiler": "bun run build:dist && bun test --preload orez-sync-cf-host/bun-wasm-loader query-compiler.test.mjs && node vite-ssr-query-compiler-test.mjs",
57
+ "test:restart": "bun run build:dist && bun restart-test.mjs",
58
+ "test:platform": "bun run build:dist:platform && bun platform-test.mjs",
59
+ "typecheck": "bun run build:dist:platform && tsc --noEmit"
29
60
  },
30
61
  "devDependencies": {
31
62
  "@cloudflare/workers-types": "4.20260617.1",
@@ -35,7 +66,16 @@
35
66
  "workerd": "1.20260617.1",
36
67
  "wrangler": "4.103.0"
37
68
  },
69
+ "peerDependencies": {
70
+ "vite": ">=5"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "vite": {
74
+ "optional": true
75
+ }
76
+ },
38
77
  "files": [
39
- "src"
78
+ "dist",
79
+ "README.md"
40
80
  ]
41
81
  }
@@ -1,16 +0,0 @@
1
- import { Buffer } from 'node:buffer'
2
-
3
- import { file, plugin } from 'bun'
4
-
5
- plugin({
6
- name: 'orez-sync-cf-host-wasm',
7
- setup(build) {
8
- build.onLoad({ filter: /sync_wasm_bg\.wasm$/ }, async ({ path }) => {
9
- const bytes = Buffer.from(await file(path).arrayBuffer()).toString('base64')
10
- return {
11
- contents: `export default new WebAssembly.Module(Uint8Array.fromBase64('${bytes}'))`,
12
- loader: 'js',
13
- }
14
- })
15
- },
16
- })
package/src/config.ts DELETED
@@ -1,102 +0,0 @@
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
- if (typeof config.authorizeWake !== 'function') {
14
- throw new TypeError('sync host config authorizeWake is required')
15
- }
16
- if (typeof config.authorizeNotify !== 'function') {
17
- throw new TypeError('sync host config authorizeNotify is required')
18
- }
19
- const hasMutators = config.mutators !== undefined
20
- const hasDelegate = config.mutateUrl !== undefined
21
- if (hasMutators === hasDelegate) {
22
- throw new TypeError('sync host config requires exactly one of mutators or mutateUrl')
23
- }
24
- if (hasDelegate && !config.upstream) {
25
- throw new TypeError('sync host config mutateUrl requires upstream')
26
- }
27
- if (config.mutateBinding !== undefined && !hasDelegate) {
28
- throw new TypeError('sync host config mutateBinding requires mutateUrl')
29
- }
30
- if (config.mutateOrigin !== undefined && !hasDelegate) {
31
- throw new TypeError('sync host config mutateOrigin requires mutateUrl')
32
- }
33
- if (config.delegatedPushRetry !== undefined && !hasDelegate) {
34
- throw new TypeError('sync host config delegatedPushRetry requires mutateUrl')
35
- }
36
- if (config.mutateBinding !== undefined && !config.mutateBinding) {
37
- throw new TypeError('sync host config mutateBinding must not be empty')
38
- }
39
- if (hasMutators && config.upstream) {
40
- throw new TypeError(
41
- 'sync host config cannot combine local mutators with upstream ingest'
42
- )
43
- }
44
- if (config.mutateUrl && !config.mutateUrl.startsWith('/')) {
45
- throw new TypeError('mutateUrl must be an absolute path')
46
- }
47
- if (config.mutateOrigin !== undefined) {
48
- let origin: URL
49
- try {
50
- origin = new URL(config.mutateOrigin)
51
- } catch {
52
- throw new TypeError('mutateOrigin must be an absolute http(s) origin')
53
- }
54
- if (
55
- (origin.protocol !== 'http:' && origin.protocol !== 'https:') ||
56
- origin.origin !== config.mutateOrigin
57
- ) {
58
- throw new TypeError('mutateOrigin must be an absolute http(s) origin')
59
- }
60
- }
61
- if (config.upstream) {
62
- if (!config.upstream.binding) throw new TypeError('upstream.binding is required')
63
- const limit = config.upstream.changeLimit ?? 1_000
64
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000) {
65
- throw new TypeError('upstream.changeLimit must be a safe integer in 1..10000')
66
- }
67
- const interval = config.upstream.intervalMs ?? 15_000
68
- if (!Number.isSafeInteger(interval) || interval < 1_000) {
69
- throw new TypeError('upstream.intervalMs must be a safe integer >= 1000')
70
- }
71
- for (const [name, value] of Object.entries({
72
- ingestBudgetRows: config.upstream.ingestBudgetRows ?? 150_000,
73
- ingestBudgetWindowMs: config.upstream.ingestBudgetWindowMs ?? 300_000,
74
- ingestBackoffMs: config.upstream.ingestBackoffMs ?? 1_000,
75
- ingestMaxBackoffMs: config.upstream.ingestMaxBackoffMs ?? 60_000,
76
- })) {
77
- if (!Number.isSafeInteger(value) || value < 1)
78
- throw new TypeError(`upstream.${name} must be a positive safe integer`)
79
- }
80
- }
81
- if (config.delegatedPushRetry) {
82
- for (const [name, value] of Object.entries({
83
- maxAttempts: config.delegatedPushRetry.maxAttempts ?? 3,
84
- initialBackoffMs: config.delegatedPushRetry.initialBackoffMs ?? 100,
85
- maxBackoffMs: config.delegatedPushRetry.maxBackoffMs ?? 1_000,
86
- timeoutMs: config.delegatedPushRetry.timeoutMs ?? 5_000,
87
- })) {
88
- if (!Number.isSafeInteger(value) || value < 1)
89
- throw new TypeError(`delegatedPushRetry.${name} must be a positive safe integer`)
90
- }
91
- }
92
- if (config.transactionQueryBudget) {
93
- for (const [name, value] of Object.entries(config.transactionQueryBudget)) {
94
- if (!Number.isSafeInteger(value) || Number(value) < 1) {
95
- throw new TypeError(
96
- `transactionQueryBudget.${name} must be a positive safe integer`
97
- )
98
- }
99
- }
100
- }
101
- return config
102
- }