orez-sync-cf-host 0.5.30 → 0.5.32
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 +10 -14
- package/dist/config.d.ts +2 -1
- package/dist/config.js +3 -0
- package/dist/generated/sync_wasm_bg.wasm +0 -0
- package/dist/host.d.ts +3 -2
- package/dist/host.js +87 -95
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/query-compiler.d.ts +2 -2
- package/dist/sql-storage-adapter.d.ts +7 -5
- package/dist/transaction-query.d.ts +0 -4
- package/dist/types.d.ts +11 -70
- package/dist/types.js +1 -5
- package/dist/visibility.d.ts +12 -0
- package/dist/visibility.js +30 -0
- package/package.json +10 -10
- package/dist/mutation-error.d.ts +0 -9
- package/dist/mutation-error.js +0 -23
- package/dist/post-commit.d.ts +0 -13
- package/dist/post-commit.js +0 -31
package/README.md
CHANGED
|
@@ -2,26 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
This is the production-shaped Cloudflare Durable Object host for the Rust sync
|
|
4
4
|
engine. The package exposes a consumer integration surface (`createSyncWorker`,
|
|
5
|
-
`createSyncDurableObject
|
|
5
|
+
`createSyncDurableObject`) and a harness deployment built
|
|
6
6
|
from `src/harness-worker.ts`. Consumers provide a JSON Zero schema, application
|
|
7
|
-
DDL/seed initializer, normalized-claims authenticator,
|
|
8
|
-
optional row-visibility hook.
|
|
7
|
+
DDL/seed initializer, normalized-claims authenticator, an
|
|
8
|
+
`orez-sync-executor` mutator registry, and an optional row-visibility hook.
|
|
9
9
|
|
|
10
|
-
The Worker authenticates at the consumer edge
|
|
10
|
+
The Worker authenticates and authorizes at the consumer edge, then forwards normalized claims
|
|
11
11
|
inside the binding request body, where request-header observability cannot record
|
|
12
12
|
them. It never logs tokens, mutation arguments, or row contents. Pull runs
|
|
13
|
-
`engine_handle_pull` inside `transactionSync`; push
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`
|
|
13
|
+
`engine_handle_pull` inside `transactionSync`; local push delegates validation,
|
|
14
|
+
replay, LMID advancement, CRUD, and committed effects to `orez-sync-executor`
|
|
15
|
+
inside `ctx.storage.transaction`. Every SQL cursor is materialized before an
|
|
16
|
+
await. Mutators may await only their application transaction operations;
|
|
17
|
+
timers, fetches, and other external work belong in
|
|
18
|
+
`ctx.defer`, which runs only after commit. Application failures use the
|
|
19
19
|
required second transaction to advance the LMID marker.
|
|
20
20
|
|
|
21
|
-
The root `orez/cf-do` executor and this host consume the same `post-commit`
|
|
22
|
-
module, so transaction retries discard effects from abandoned attempts in both
|
|
23
|
-
paths.
|
|
24
|
-
|
|
25
21
|
Transaction-query `ILIKE` folding is ASCII-only on Durable Object SQLite;
|
|
26
22
|
non-ASCII case pairs can diverge from PostgreSQL.
|
|
27
23
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { PullCaps, SyncHostConfig, SyncHostEnv } from './types.js';
|
|
2
|
+
import type { Schema } from '@rocicorp/zero';
|
|
2
3
|
export declare function validatePullCaps(caps: PullCaps): PullCaps;
|
|
3
|
-
export declare function validateSyncHostConfig<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): SyncHostConfig<Env>;
|
|
4
|
+
export declare function validateSyncHostConfig<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): SyncHostConfig<Env, S>;
|
package/dist/config.js
CHANGED
|
@@ -11,6 +11,9 @@ export function validateSyncHostConfig(config) {
|
|
|
11
11
|
if (typeof config.authorizeNotify !== 'function') {
|
|
12
12
|
throw new TypeError('sync host config authorizeNotify is required');
|
|
13
13
|
}
|
|
14
|
+
if (typeof config.authorize !== 'function') {
|
|
15
|
+
throw new TypeError('sync host config authorize is required');
|
|
16
|
+
}
|
|
14
17
|
const hasMutators = config.mutators !== undefined;
|
|
15
18
|
const hasDelegate = config.mutateUrl !== undefined;
|
|
16
19
|
if (hasMutators === hasDelegate) {
|
|
Binary file
|
package/dist/host.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
2
|
import type { SyncHostConfig, SyncHostEnv } from './types.js';
|
|
3
|
+
import type { Schema } from '@rocicorp/zero';
|
|
3
4
|
/**
|
|
4
5
|
* Create the consumer-facing Worker router. Authentication happens here; the
|
|
5
6
|
* Durable Object receives normalized claims inside the binding request body so
|
|
6
7
|
* observability systems cannot record them as request-header metadata.
|
|
7
8
|
*/
|
|
8
|
-
export declare function createSyncWorker<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): ExportedHandler<Env>;
|
|
9
|
+
export declare function createSyncWorker<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): ExportedHandler<Env>;
|
|
9
10
|
export interface SyncDurableObjectConstructor<Env extends SyncHostEnv> {
|
|
10
11
|
new (ctx: DurableObjectState, env: Env): DurableObject<Env>;
|
|
11
12
|
}
|
|
12
13
|
/** Create the namespace Durable Object class for one bundled consumer config. */
|
|
13
|
-
export declare function createSyncDurableObject<Env extends SyncHostEnv>(config: SyncHostConfig<Env>): SyncDurableObjectConstructor<Env>;
|
|
14
|
+
export declare function createSyncDurableObject<Env extends SyncHostEnv, S extends Schema = Schema>(config: SyncHostConfig<Env, S>): SyncDurableObjectConstructor<Env>;
|
package/dist/host.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
import { createSyncExecutor } from 'orez-sync-executor/core';
|
|
2
3
|
import { validatePullCaps, validateSyncHostConfig } from './config.js';
|
|
3
|
-
import { createPostCommitEffects } from './post-commit.js';
|
|
4
4
|
import { createQueryCompiler } from './query-compiler.js';
|
|
5
5
|
import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlStorageSyncDb, } from './sql-storage-adapter.js';
|
|
6
|
-
import {
|
|
7
|
-
import { engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_assemble_push_response, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, engine_handle_pull, engine_handle_query_pull, engine_init_query_schema, engine_init_schema, engine_invalidate, engine_memory_bytes, engine_preflight, engine_prune, engine_push_validate, engine_read_snapshot_progress, engine_record_app_error, engine_state, engine_version, } from './wasm.js';
|
|
6
|
+
import { engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, engine_handle_pull, engine_handle_query_pull, engine_init_query_schema, engine_init_schema, engine_invalidate, engine_memory_bytes, engine_preflight, engine_prune, engine_push_validate, engine_read_snapshot_progress, engine_state, engine_version, } from './wasm.js';
|
|
8
7
|
import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
|
|
9
8
|
const NAMESPACE_HEADER = 'x-orez-sync-namespace';
|
|
10
9
|
const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
|
|
@@ -177,8 +176,12 @@ export function createSyncWorker(config) {
|
|
|
177
176
|
let forwardedBody = null;
|
|
178
177
|
if (!isAdmin && route !== '/wake' && route !== '/notify') {
|
|
179
178
|
const claims = await config.authenticate(request, env);
|
|
180
|
-
if (!claims)
|
|
179
|
+
if (!claims || typeof claims.userID !== 'string' || claims.userID.length === 0) {
|
|
181
180
|
return json({ error: 'missing authentication' }, 401);
|
|
181
|
+
}
|
|
182
|
+
if (!(await config.authorize(request, claims, namespace, env))) {
|
|
183
|
+
return json({ error: 'forbidden' }, 403);
|
|
184
|
+
}
|
|
182
185
|
if ((route === '/pull' || route === '/push') && request.method === 'POST') {
|
|
183
186
|
try {
|
|
184
187
|
const body = await requestObject(request);
|
|
@@ -231,6 +234,8 @@ export function createSyncDurableObject(config) {
|
|
|
231
234
|
#engineDb;
|
|
232
235
|
#directSql;
|
|
233
236
|
#mutatorSql;
|
|
237
|
+
#executor;
|
|
238
|
+
#executorBeforeCommitFault = null;
|
|
234
239
|
#bootID = crypto.randomUUID();
|
|
235
240
|
#lastRequestAt = 0;
|
|
236
241
|
#hibernations = 0;
|
|
@@ -259,6 +264,50 @@ export function createSyncDurableObject(config) {
|
|
|
259
264
|
this.#engineDb = new SqlStorageSyncDb(ctx.storage.sql, recordRowsWritten);
|
|
260
265
|
this.#directSql = new SqlStorageDirect(ctx.storage.sql, recordRowsWritten);
|
|
261
266
|
this.#mutatorSql = new SqlStorageMutatorTransaction(this.#directSql, (ast, format) => this.#wasm(() => compileQuery(ast, format)), config.transactionQueryBudget);
|
|
267
|
+
const database = {
|
|
268
|
+
dialect: 'sqlite',
|
|
269
|
+
transaction: async (work) => this.ctx.storage.transaction(async () => {
|
|
270
|
+
let applicationWrite = false;
|
|
271
|
+
const tx = {
|
|
272
|
+
exec: async (sql, params, metadata) => {
|
|
273
|
+
if (metadata !== undefined ||
|
|
274
|
+
(!/^\s*CREATE\s+(?:SCHEMA|TABLE)\b/i.test(sql) &&
|
|
275
|
+
!/\b_zsync_[A-Za-z0-9_]+\b/.test(sql))) {
|
|
276
|
+
applicationWrite = true;
|
|
277
|
+
}
|
|
278
|
+
return this.#mutatorSql.exec(sql, params, metadata);
|
|
279
|
+
},
|
|
280
|
+
query: (sql, params) => this.#mutatorSql.query(sql, params),
|
|
281
|
+
queryAst: (ast, format, queryName) => this.#mutatorSql.queryAst(ast, format, queryName),
|
|
282
|
+
};
|
|
283
|
+
const value = await work(tx);
|
|
284
|
+
if (applicationWrite && this.#executorBeforeCommitFault) {
|
|
285
|
+
const fault = this.#executorBeforeCommitFault;
|
|
286
|
+
this.#executorBeforeCommitFault = null;
|
|
287
|
+
throw this.#faultError(fault, 'push_after_write_before_commit');
|
|
288
|
+
}
|
|
289
|
+
return value;
|
|
290
|
+
}),
|
|
291
|
+
query: (sql, params) => this.#mutatorSql.query(sql, params),
|
|
292
|
+
};
|
|
293
|
+
this.#executor = config.mutators
|
|
294
|
+
? createSyncExecutor({
|
|
295
|
+
database,
|
|
296
|
+
effects: {
|
|
297
|
+
runBackground: (promise) => this.ctx.waitUntil(promise),
|
|
298
|
+
report: (error) => {
|
|
299
|
+
this.#counters.externalEffectFailures++;
|
|
300
|
+
console.error(JSON.stringify({
|
|
301
|
+
event: 'sync_external_effect_error',
|
|
302
|
+
hostVersion: config.hostVersion,
|
|
303
|
+
error: errorMessage(error),
|
|
304
|
+
}));
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
mutators: config.mutators,
|
|
308
|
+
schema: config.schema,
|
|
309
|
+
})
|
|
310
|
+
: null;
|
|
262
311
|
ctx.blockConcurrencyWhile(async () => {
|
|
263
312
|
ctx.storage.transactionSync(() => {
|
|
264
313
|
config.initialize(this.#directSql);
|
|
@@ -329,7 +378,16 @@ export function createSyncDurableObject(config) {
|
|
|
329
378
|
filters: Object.keys(config.schema.tables).flatMap((table) => {
|
|
330
379
|
const filter = config.visibility?.filter(table, claims);
|
|
331
380
|
return filter
|
|
332
|
-
? [
|
|
381
|
+
? [
|
|
382
|
+
filter.kind === 'expression'
|
|
383
|
+
? { kind: 'expression', table, expression: filter.expression }
|
|
384
|
+
: {
|
|
385
|
+
kind: 'raw',
|
|
386
|
+
table,
|
|
387
|
+
sql: filter.sql,
|
|
388
|
+
params: [...(filter.params ?? [])],
|
|
389
|
+
},
|
|
390
|
+
]
|
|
333
391
|
: [];
|
|
334
392
|
}),
|
|
335
393
|
};
|
|
@@ -1117,99 +1175,34 @@ export function createSyncDurableObject(config) {
|
|
|
1117
1175
|
const beforeMutationFault = this.#takeFault('push_before_mutation');
|
|
1118
1176
|
if (beforeMutationFault)
|
|
1119
1177
|
throw this.#faultError(beforeMutationFault, 'push_before_mutation');
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
return decision;
|
|
1140
|
-
const mutator = config.mutators?.[mutation.name];
|
|
1141
|
-
if (!mutator)
|
|
1142
|
-
throw new Error(`unknown mutator: ${mutation.name}`);
|
|
1143
|
-
await mutator(this.#mutatorSql, mutation.args[0] ?? null, {
|
|
1144
|
-
claims,
|
|
1145
|
-
clientID: mutation.clientID,
|
|
1146
|
-
mutationID: mutation.id,
|
|
1147
|
-
defer: deferred.defer,
|
|
1148
|
-
});
|
|
1149
|
-
if (beforeCommitFault)
|
|
1150
|
-
throw this.#faultError(beforeCommitFault, 'push_after_write_before_commit');
|
|
1151
|
-
this.#wasm(() => engine_finalize(this.#engineDb, plan.clientGroupID, mutation.clientID, mutation.id));
|
|
1152
|
-
return decision;
|
|
1153
|
-
});
|
|
1154
|
-
transactionMs += performance.now() - txStarted;
|
|
1155
|
-
if (preflight.kind === 'replay') {
|
|
1156
|
-
const expected = preflight
|
|
1157
|
-
.expected;
|
|
1158
|
-
results.push({
|
|
1159
|
-
clientID: mutation.clientID,
|
|
1160
|
-
id: mutation.id,
|
|
1161
|
-
result: {
|
|
1162
|
-
error: 'alreadyProcessed',
|
|
1163
|
-
details: `Ignoring mutation from ${mutation.clientID} with ID ${mutation.id} as it was already processed. Expected: ${expected}`,
|
|
1164
|
-
},
|
|
1165
|
-
});
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
lmidAdvances++;
|
|
1169
|
-
results.push({
|
|
1170
|
-
clientID: mutation.clientID,
|
|
1171
|
-
id: mutation.id,
|
|
1172
|
-
result: {},
|
|
1173
|
-
});
|
|
1174
|
-
// Keep the advisory fan-out off the push response's critical path.
|
|
1175
|
-
// waitUntil anchors the coalescing timer across request completion,
|
|
1176
|
-
// while the next serialized client push can join the same batch.
|
|
1177
|
-
this.ctx.waitUntil(this.#enqueueWake(mutation.clientID));
|
|
1178
|
-
await deferred.runAfterCommit((error) => {
|
|
1179
|
-
this.#counters.externalEffectFailures++;
|
|
1180
|
-
console.error(JSON.stringify({
|
|
1181
|
-
event: 'sync_external_effect_error',
|
|
1182
|
-
hostVersion: config.hostVersion,
|
|
1183
|
-
error: errorMessage(error),
|
|
1184
|
-
}));
|
|
1185
|
-
});
|
|
1178
|
+
if (!this.#executor)
|
|
1179
|
+
throw new Error('local sync executor is not configured');
|
|
1180
|
+
// Consume the fault outside the transaction it aborts so a rollback
|
|
1181
|
+
// cannot restore the one-shot control flag.
|
|
1182
|
+
this.#executorBeforeCommitFault = this.#takeFault('push_after_write_before_commit');
|
|
1183
|
+
const txStarted = performance.now();
|
|
1184
|
+
let result;
|
|
1185
|
+
try {
|
|
1186
|
+
result = await this.#executor.push(body, claims);
|
|
1187
|
+
}
|
|
1188
|
+
finally {
|
|
1189
|
+
this.#executorBeforeCommitFault = null;
|
|
1190
|
+
}
|
|
1191
|
+
transactionMs += performance.now() - txStarted;
|
|
1192
|
+
const mutationResults = 'mutations' in result.pushResponse ? result.pushResponse.mutations : [];
|
|
1193
|
+
for (const mutation of mutationResults) {
|
|
1194
|
+
if ('error' in mutation.result &&
|
|
1195
|
+
mutation.result.error === 'alreadyProcessed') {
|
|
1196
|
+
continue;
|
|
1186
1197
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if (!isAppError)
|
|
1190
|
-
throw error;
|
|
1198
|
+
lmidAdvances++;
|
|
1199
|
+
if ('error' in mutation.result && mutation.result.error === 'app') {
|
|
1191
1200
|
this.#counters.applicationErrors++;
|
|
1192
1201
|
resultClass = 'application_error';
|
|
1193
|
-
const appError = error;
|
|
1194
|
-
const txStarted = performance.now();
|
|
1195
|
-
await this.ctx.storage.transaction(async () => {
|
|
1196
|
-
this.#wasm(() => engine_record_app_error(this.#engineDb, plan.clientGroupID, mutation.clientID, mutation.id, claims.userID));
|
|
1197
|
-
});
|
|
1198
|
-
transactionMs += performance.now() - txStarted;
|
|
1199
|
-
lmidAdvances++;
|
|
1200
|
-
results.push({
|
|
1201
|
-
clientID: mutation.clientID,
|
|
1202
|
-
id: mutation.id,
|
|
1203
|
-
result: {
|
|
1204
|
-
error: 'app',
|
|
1205
|
-
message: appError.message,
|
|
1206
|
-
details: appError.details ?? appError.message,
|
|
1207
|
-
},
|
|
1208
|
-
});
|
|
1209
|
-
this.ctx.waitUntil(this.#enqueueWake(mutation.clientID));
|
|
1210
1202
|
}
|
|
1203
|
+
this.ctx.waitUntil(this.#enqueueWake(mutation.id.clientID));
|
|
1211
1204
|
}
|
|
1212
|
-
if (
|
|
1205
|
+
if (mutationResults.length > 0) {
|
|
1213
1206
|
const txStarted = performance.now();
|
|
1214
1207
|
await this.ctx.storage.transaction(async () => {
|
|
1215
1208
|
this.#wasm(() => engine_prune(this.#engineDb, this.#retainChanges()));
|
|
@@ -1220,7 +1213,6 @@ export function createSyncDurableObject(config) {
|
|
|
1220
1213
|
const afterCommitFault = this.#takeFault('push_after_commit_before_response');
|
|
1221
1214
|
if (afterCommitFault)
|
|
1222
1215
|
throw this.#faultError(afterCommitFault, 'push_after_commit_before_response');
|
|
1223
|
-
const response = this.#wasm(() => engine_assemble_push_response(results));
|
|
1224
1216
|
const state = this.#engineStateBestEffort();
|
|
1225
1217
|
this.#log({
|
|
1226
1218
|
namespaceHash: namespace,
|
|
@@ -1246,7 +1238,7 @@ export function createSyncDurableObject(config) {
|
|
|
1246
1238
|
this.#dropNextPushResponse = false;
|
|
1247
1239
|
return json({ error: 'intentionally dropped push response' }, 503);
|
|
1248
1240
|
}
|
|
1249
|
-
return json(
|
|
1241
|
+
return json(result);
|
|
1250
1242
|
}
|
|
1251
1243
|
catch (error) {
|
|
1252
1244
|
const status = statusOf(error);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { createSyncDurableObject, createSyncWorker } from './host.js';
|
|
2
2
|
export { createQueryCompiler } from './query-compiler.js';
|
|
3
|
-
export {
|
|
3
|
+
export { visibility } from './types.js';
|
|
4
4
|
export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
|
|
5
|
-
export type {
|
|
5
|
+
export type { PullCaps, QueryResolver, SyncHostConfig, SyncHostEnv, ServiceBinding, SyncSql, UpstreamConfig, VisibilityConfig, VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from './types.js';
|
|
6
6
|
export type { SyncDurableObjectConstructor } from './host.js';
|
|
7
|
-
export type { CompiledTransactionQueryNode, CompiledTransactionQueryPlan, CompiledTransactionQueryRelationship, TransactionQueryBinding, TransactionQueryBudget, TransactionQueryColumn, TransactionQueryColumnType, TransactionQueryExecutionOptions,
|
|
7
|
+
export type { CompiledTransactionQueryNode, CompiledTransactionQueryPlan, CompiledTransactionQueryRelationship, TransactionQueryBinding, TransactionQueryBudget, TransactionQueryColumn, TransactionQueryColumnType, TransactionQueryExecutionOptions, TransactionQueryWireValue, } from './transaction-query.js';
|
|
8
8
|
export type { TransactionQueryCompiler } from './query-compiler.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createSyncDurableObject, createSyncWorker } from './host.js';
|
|
2
2
|
export { createQueryCompiler } from './query-compiler.js';
|
|
3
|
-
export {
|
|
3
|
+
export { visibility } from './types.js';
|
|
4
4
|
export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
|
package/dist/query-compiler.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompiledTransactionQueryPlan
|
|
2
|
-
import type { ZeroSchemaConfig } from '
|
|
1
|
+
import type { CompiledTransactionQueryPlan } from './transaction-query.js';
|
|
2
|
+
import type { TransactionQueryFormat, ZeroSchemaConfig } from 'orez-sync-executor';
|
|
3
3
|
export type TransactionQueryCompiler = (ast: unknown, format: TransactionQueryFormat) => CompiledTransactionQueryPlan;
|
|
4
4
|
export declare function createQueryCompiler(schema: ZeroSchemaConfig): TransactionQueryCompiler;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { CompiledTransactionQueryPlan, TransactionQueryBudget
|
|
2
|
-
import type {
|
|
1
|
+
import type { CompiledTransactionQueryPlan, TransactionQueryBudget } from './transaction-query.js';
|
|
2
|
+
import type { SyncSql } from './types.js';
|
|
3
|
+
import type { ApplicationTransaction, ExecResult, SqlStatementMetadata } from 'orez-sync-executor';
|
|
4
|
+
import type { TransactionQueryFormat } from 'orez-sync-executor';
|
|
3
5
|
type WireValue = {
|
|
4
6
|
kind: 'null';
|
|
5
7
|
} | {
|
|
@@ -49,7 +51,7 @@ export declare class SqlStorageDirect implements SyncSql {
|
|
|
49
51
|
private readonly sql;
|
|
50
52
|
private readonly recordRowsWritten;
|
|
51
53
|
constructor(sql: SqlStorage, recordRowsWritten?: (rows: number) => void);
|
|
52
|
-
exec(sql: string, params?: readonly unknown[], _metadata?: SqlStatementMetadata):
|
|
54
|
+
exec(sql: string, params?: readonly unknown[], _metadata?: SqlStatementMetadata): ExecResult;
|
|
53
55
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
@@ -57,12 +59,12 @@ export declare class SqlStorageDirect implements SyncSql {
|
|
|
57
59
|
* materializes its SqlStorage operation before its promise resolves, so an
|
|
58
60
|
* existing `await tx.query(...)` cannot carry a cursor across an await.
|
|
59
61
|
*/
|
|
60
|
-
export declare class SqlStorageMutatorTransaction implements
|
|
62
|
+
export declare class SqlStorageMutatorTransaction implements ApplicationTransaction {
|
|
61
63
|
private readonly direct;
|
|
62
64
|
private readonly compileQuery;
|
|
63
65
|
private readonly queryBudget?;
|
|
64
66
|
constructor(direct: SqlStorageDirect, compileQuery: (ast: unknown, format: TransactionQueryFormat) => CompiledTransactionQueryPlan, queryBudget?: Partial<TransactionQueryBudget> | undefined);
|
|
65
|
-
exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<
|
|
67
|
+
exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<ExecResult>;
|
|
66
68
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<Row[]>;
|
|
67
69
|
queryAst<Result = unknown>(ast: unknown, format: TransactionQueryFormat, queryName?: string): Promise<Result>;
|
|
68
70
|
}
|
|
@@ -50,10 +50,6 @@ export type TransactionQueryExecutionOptions = {
|
|
|
50
50
|
queryName?: string;
|
|
51
51
|
budget?: Partial<TransactionQueryBudget>;
|
|
52
52
|
};
|
|
53
|
-
export type TransactionQueryFormat = {
|
|
54
|
-
singular: boolean;
|
|
55
|
-
relationships: Readonly<Record<string, TransactionQueryFormat>>;
|
|
56
|
-
};
|
|
57
53
|
export declare const DEFAULT_TRANSACTION_QUERY_BUDGET: TransactionQueryBudget;
|
|
58
54
|
export declare class TransactionQueryBudgetError extends Error {
|
|
59
55
|
readonly query: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,73 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export type
|
|
5
|
-
export type
|
|
6
|
-
[key: string]: JsonValue;
|
|
7
|
-
};
|
|
8
|
-
export type NormalizedClaims = {
|
|
9
|
-
/** Stable consumer user id used for client-group ownership. */
|
|
10
|
-
userID: string;
|
|
11
|
-
[claim: string]: JsonValue;
|
|
12
|
-
};
|
|
13
|
-
export type ZeroSchemaConfig = {
|
|
14
|
-
readonly tables: Readonly<Record<string, {
|
|
15
|
-
readonly name?: string;
|
|
16
|
-
readonly serverName?: string;
|
|
17
|
-
readonly columns: Readonly<Record<string, {
|
|
18
|
-
readonly type: string;
|
|
19
|
-
readonly serverName?: string;
|
|
20
|
-
}>>;
|
|
21
|
-
readonly primaryKey: readonly string[];
|
|
22
|
-
}>>;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Explicit identity for a SQLite write to a published application table.
|
|
26
|
-
*
|
|
27
|
-
* The physical table installs CDC. `publicTable` is the logical Zero name
|
|
28
|
-
* emitted in the change feed. This replaces metadata recovered by parsing
|
|
29
|
-
* PostgreSQL statements.
|
|
30
|
-
*/
|
|
31
|
-
export type SqlStatementMetadata = {
|
|
32
|
-
table: string;
|
|
33
|
-
publicTable: string;
|
|
34
|
-
kind: 'insert' | 'update' | 'delete' | 'upsert';
|
|
35
|
-
};
|
|
36
|
-
export type SQLiteExecResult = {
|
|
37
|
-
changes: number;
|
|
38
|
-
};
|
|
1
|
+
import type { TransactionQueryBudget } from './transaction-query.js';
|
|
2
|
+
import type { Schema } from '@rocicorp/zero';
|
|
3
|
+
import type { ExecResult, JsonValue, MutatorRegistry, NormalizedClaims, SqlStatementMetadata, VisibilityConfig } from 'orez-sync-executor';
|
|
4
|
+
export { visibility, type VisibilityExpression, type VisibilityFilter, type VisibilityOperand, type VisibilityValue, } from './visibility.js';
|
|
5
|
+
export type { VisibilityConfig } from 'orez-sync-executor';
|
|
39
6
|
export interface SyncSql {
|
|
40
|
-
exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata):
|
|
7
|
+
exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): ExecResult;
|
|
41
8
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
|
|
42
9
|
}
|
|
43
|
-
export interface MutatorSql {
|
|
44
|
-
exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): Promise<SQLiteExecResult>;
|
|
45
|
-
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<Row[]>;
|
|
46
|
-
/** Execute a validated Zero AST inside the current application transaction. */
|
|
47
|
-
queryAst<Result = unknown>(ast: JsonValue, format: TransactionQueryFormat, queryName?: string): Promise<Result>;
|
|
48
|
-
}
|
|
49
|
-
export type MutatorContext = {
|
|
50
|
-
claims: NormalizedClaims;
|
|
51
|
-
/** Current Zero mutation identity, when invoked by the production host. */
|
|
52
|
-
clientID?: string;
|
|
53
|
-
mutationID?: string | number;
|
|
54
|
-
defer(effect: DeferredEffect): void;
|
|
55
|
-
};
|
|
56
|
-
export type RegisteredMutator = (tx: MutatorSql, args: JsonValue, context: MutatorContext) => void | Promise<void>;
|
|
57
|
-
export type MutatorRegistry = Readonly<Record<string, RegisteredMutator>>;
|
|
58
|
-
/** Preserve mutator names while making the host-facing registry immutable. */
|
|
59
|
-
export declare function registerMutators<const Registry extends Record<string, RegisteredMutator>>(registry: Registry): Readonly<Registry>;
|
|
60
|
-
export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js';
|
|
61
|
-
export type VisibilityFilter = {
|
|
62
|
-
/** SQL WHERE fragment only, without the WHERE keyword. */
|
|
63
|
-
sql: string;
|
|
64
|
-
params?: readonly JsonPrimitive[];
|
|
65
|
-
};
|
|
66
|
-
export type VisibilityConfig = {
|
|
67
|
-
/** True only when every predicate depends on the selected row alone. */
|
|
68
|
-
rowLocal: boolean | ((claims: NormalizedClaims) => boolean);
|
|
69
|
-
filter(table: string, claims: NormalizedClaims): VisibilityFilter | undefined;
|
|
70
|
-
};
|
|
71
10
|
export type QueryResolver = (name: string, args: readonly JsonValue[], claims: NormalizedClaims, env: SyncHostEnv) => JsonValue | Promise<JsonValue>;
|
|
72
11
|
export type PullCaps = {
|
|
73
12
|
maxChangeRows: number;
|
|
@@ -108,10 +47,10 @@ export type DelegatedPushRetryConfig = {
|
|
|
108
47
|
/** Per-attempt service-binding timeout. Defaults to 5,000ms. */
|
|
109
48
|
timeoutMs?: number;
|
|
110
49
|
};
|
|
111
|
-
export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
|
|
50
|
+
export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Schema = Schema> = {
|
|
112
51
|
hostVersion: string;
|
|
113
|
-
schema:
|
|
114
|
-
mutators?: MutatorRegistry
|
|
52
|
+
schema: S;
|
|
53
|
+
mutators?: MutatorRegistry<S>;
|
|
115
54
|
/**
|
|
116
55
|
* absolute app push path on the delegated mutation service. a successful
|
|
117
56
|
* response must be causally visible through the configured upstream data
|
|
@@ -132,6 +71,8 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv> = {
|
|
|
132
71
|
/** Application DDL and optional seed, called before sync-core schema init. */
|
|
133
72
|
initialize(sql: SyncSql): void;
|
|
134
73
|
authenticate(request: Request, env: Env): NormalizedClaims | null | Promise<NormalizedClaims | null>;
|
|
74
|
+
/** Authorize authenticated application access before selecting a namespace DO. */
|
|
75
|
+
authorize(request: Request, claims: NormalizedClaims, namespace: string, env: Env): boolean | Promise<boolean>;
|
|
135
76
|
/** Authorize the advisory wake socket before selecting a namespace DO.
|
|
136
77
|
* Browser clients should present a short-lived, namespace-scoped capability
|
|
137
78
|
* in the query string because WebSocket cannot set request headers. */
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function registerMutators(registry) {
|
|
3
|
-
return Object.freeze({ ...registry });
|
|
4
|
-
}
|
|
5
|
-
export { MutationApplicationError, isMutationApplicationError } from './mutation-error.js';
|
|
1
|
+
export { visibility, } from './visibility.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue } from 'orez-sync-executor';
|
|
2
|
+
export type { VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from 'orez-sync-executor';
|
|
3
|
+
export declare const visibility: {
|
|
4
|
+
readonly column: (table: string, column: string, qualifier?: string) => VisibilityOperand;
|
|
5
|
+
readonly value: (value: VisibilityValue) => VisibilityOperand;
|
|
6
|
+
readonly comparison: (left: VisibilityOperand, operator: "=" | "!=" | "<" | ">" | "<=" | ">=" | "IS" | "IS NOT", right: VisibilityOperand) => VisibilityExpression;
|
|
7
|
+
readonly and: (...conditions: VisibilityExpression[]) => VisibilityExpression;
|
|
8
|
+
readonly or: (...conditions: VisibilityExpression[]) => VisibilityExpression;
|
|
9
|
+
readonly exists: (table: string, where: VisibilityExpression, qualifier?: string) => VisibilityExpression;
|
|
10
|
+
readonly filter: (expression: VisibilityExpression) => VisibilityFilter;
|
|
11
|
+
readonly raw: (sql: string, params?: readonly VisibilityValue[]) => VisibilityFilter;
|
|
12
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const visibility = {
|
|
2
|
+
column(table, column, qualifier) {
|
|
3
|
+
return qualifier
|
|
4
|
+
? { type: 'column', table, column, qualifier }
|
|
5
|
+
: { type: 'column', table, column };
|
|
6
|
+
},
|
|
7
|
+
value(value) {
|
|
8
|
+
return { type: 'value', value };
|
|
9
|
+
},
|
|
10
|
+
comparison(left, operator, right) {
|
|
11
|
+
return { type: 'comparison', operator, left, right };
|
|
12
|
+
},
|
|
13
|
+
and(...conditions) {
|
|
14
|
+
return { type: 'and', conditions };
|
|
15
|
+
},
|
|
16
|
+
or(...conditions) {
|
|
17
|
+
return { type: 'or', conditions };
|
|
18
|
+
},
|
|
19
|
+
exists(table, where, qualifier) {
|
|
20
|
+
return qualifier
|
|
21
|
+
? { type: 'exists', table, qualifier, where }
|
|
22
|
+
: { type: 'exists', table, where };
|
|
23
|
+
},
|
|
24
|
+
filter(expression) {
|
|
25
|
+
return { kind: 'expression', expression };
|
|
26
|
+
},
|
|
27
|
+
raw(sql, params = []) {
|
|
28
|
+
return { kind: 'raw', sql, params };
|
|
29
|
+
},
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez-sync-cf-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"import": "./dist/index.js"
|
|
9
9
|
},
|
|
10
|
-
"./post-commit": {
|
|
11
|
-
"types": "./dist/post-commit.d.ts",
|
|
12
|
-
"import": "./dist/post-commit.js"
|
|
13
|
-
},
|
|
14
|
-
"./mutation-error": {
|
|
15
|
-
"types": "./dist/mutation-error.d.ts",
|
|
16
|
-
"import": "./dist/mutation-error.js"
|
|
17
|
-
},
|
|
18
10
|
"./node-wasm-loader": {
|
|
19
11
|
"types": "./dist/node-wasm-loader.d.ts",
|
|
20
12
|
"import": "./dist/node-wasm-loader.js"
|
|
@@ -31,6 +23,10 @@
|
|
|
31
23
|
"types": "./dist/transaction-query.d.ts",
|
|
32
24
|
"import": "./dist/transaction-query.js"
|
|
33
25
|
},
|
|
26
|
+
"./visibility": {
|
|
27
|
+
"types": "./dist/visibility.d.ts",
|
|
28
|
+
"import": "./dist/visibility.js"
|
|
29
|
+
},
|
|
34
30
|
"./vite-wasm-loader": {
|
|
35
31
|
"types": "./dist/vite-wasm-loader.d.ts",
|
|
36
32
|
"import": "./dist/vite-wasm-loader.js"
|
|
@@ -49,7 +45,7 @@
|
|
|
49
45
|
"bundle": "bun run build:dist && wrangler deploy --dry-run --outdir .wrangler/bundle",
|
|
50
46
|
"measure": "bun run build:dist && bun measure.mjs",
|
|
51
47
|
"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
|
|
48
|
+
"test:config": "bun test config.test.mjs write-safeguards.test.mjs transaction-query.test.mjs",
|
|
53
49
|
"test:integration": "bun run build:dist && bun integration-test.mjs",
|
|
54
50
|
"test:ingest": "bun run build:dist && bun ingest-test.mjs",
|
|
55
51
|
"test:package": "bun run build:dist && node package-runtime-test.mjs",
|
|
@@ -58,6 +54,9 @@
|
|
|
58
54
|
"test:platform": "bun run build:dist:platform && bun platform-test.mjs",
|
|
59
55
|
"typecheck": "bun run build:dist:platform && tsc --noEmit"
|
|
60
56
|
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"orez-sync-executor": "0.5.32"
|
|
59
|
+
},
|
|
61
60
|
"devDependencies": {
|
|
62
61
|
"@cloudflare/workers-types": "4.20260617.1",
|
|
63
62
|
"@types/bun": "1.3.14",
|
|
@@ -67,6 +66,7 @@
|
|
|
67
66
|
"wrangler": "4.103.0"
|
|
68
67
|
},
|
|
69
68
|
"peerDependencies": {
|
|
69
|
+
"@rocicorp/zero": ">=1.7.0",
|
|
70
70
|
"vite": ">=5"
|
|
71
71
|
},
|
|
72
72
|
"peerDependenciesMeta": {
|
package/dist/mutation-error.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export declare class MutationApplicationError extends Error {
|
|
2
|
-
readonly details: string;
|
|
3
|
-
constructor(details: string, message?: string);
|
|
4
|
-
}
|
|
5
|
-
export declare function isMutationApplicationError(error: unknown): error is {
|
|
6
|
-
name: string;
|
|
7
|
-
message: string;
|
|
8
|
-
details: string;
|
|
9
|
-
};
|
package/dist/mutation-error.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// the app-rejection contract shared by every orez sync host and by
|
|
2
|
-
// protocol-neutral authoring layers (on-zero adapters). classification is
|
|
3
|
-
// STRUCTURAL, never instanceof: adapters must not depend on orez to reject a
|
|
4
|
-
// mutation, and instanceof breaks across package copies (dual-package
|
|
5
|
-
// installs, --into tarballs, workspace duplication). any error whose name is
|
|
6
|
-
// 'MutationApplicationError' with string details/message is claiming this
|
|
7
|
-
// contract deliberately.
|
|
8
|
-
export class MutationApplicationError extends Error {
|
|
9
|
-
details;
|
|
10
|
-
constructor(details, message = details) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.details = details;
|
|
13
|
-
this.name = 'MutationApplicationError';
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function isMutationApplicationError(error) {
|
|
17
|
-
if (typeof error !== 'object' || error === null)
|
|
18
|
-
return false;
|
|
19
|
-
const shaped = error;
|
|
20
|
-
return (shaped.name === 'MutationApplicationError' &&
|
|
21
|
-
typeof shaped.message === 'string' &&
|
|
22
|
-
typeof shaped.details === 'string');
|
|
23
|
-
}
|
package/dist/post-commit.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export type DeferredEffect = () => void | Promise<void>;
|
|
2
|
-
export type PostCommitEffects = {
|
|
3
|
-
/** discard effects from a storage-transaction attempt before starting another */
|
|
4
|
-
beginAttempt(): void;
|
|
5
|
-
defer(effect: DeferredEffect): void;
|
|
6
|
-
/** run the committed attempt's effects in order without rejecting */
|
|
7
|
-
runAfterCommit(onError: (error: unknown) => void): Promise<void>;
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* collect external work during a retryable transaction and run only the effects
|
|
11
|
-
* belonging to the attempt that commits.
|
|
12
|
-
*/
|
|
13
|
-
export declare function createPostCommitEffects(): PostCommitEffects;
|
package/dist/post-commit.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* collect external work during a retryable transaction and run only the effects
|
|
3
|
-
* belonging to the attempt that commits.
|
|
4
|
-
*/
|
|
5
|
-
export function createPostCommitEffects() {
|
|
6
|
-
const effects = [];
|
|
7
|
-
return {
|
|
8
|
-
beginAttempt() {
|
|
9
|
-
effects.length = 0;
|
|
10
|
-
},
|
|
11
|
-
defer(effect) {
|
|
12
|
-
effects.push(effect);
|
|
13
|
-
},
|
|
14
|
-
async runAfterCommit(onError) {
|
|
15
|
-
const committed = effects.splice(0);
|
|
16
|
-
for (const effect of committed) {
|
|
17
|
-
try {
|
|
18
|
-
await effect();
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
try {
|
|
22
|
-
onError(error);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// reporting must not turn a committed effect failure into a rejection
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|