orez-sync-cf-host 0.10.7 → 0.11.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/dist/host.js +19 -40
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/query-patch.d.ts +29 -0
- package/dist/query-patch.js +54 -0
- package/dist/types.d.ts +19 -3
- package/package.json +6 -2
package/dist/host.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DurableObject } from 'cloudflare:workers';
|
|
|
2
2
|
import { createSyncExecutor } from 'orez-sync-executor/core';
|
|
3
3
|
import { validatePullCaps, validateSyncHostConfig } from './config.js';
|
|
4
4
|
import { createQueryCompiler } from './query-compiler.js';
|
|
5
|
+
import { resolveQueryPatch } from './query-patch.js';
|
|
5
6
|
import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlStorageSyncDb, } from './sql-storage-adapter.js';
|
|
6
7
|
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';
|
|
7
8
|
import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
|
|
@@ -344,7 +345,7 @@ export function createSyncDurableObject(config) {
|
|
|
344
345
|
this.#ingestBreaker.restore(ingestBreakerReason, Number(this.#controlGet('ingestBreakerRetryAt')), Number(this.#controlGet('ingestBreakerTrips')));
|
|
345
346
|
}
|
|
346
347
|
this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
|
|
347
|
-
if (config.queryAware || config.
|
|
348
|
+
if (config.queryAware || config.resolveQueries)
|
|
348
349
|
this.#wasm(() => engine_init_query_schema(this.#engineDb));
|
|
349
350
|
});
|
|
350
351
|
});
|
|
@@ -952,7 +953,7 @@ export function createSyncDurableObject(config) {
|
|
|
952
953
|
const queryAware = this.#queryAwareOverride() ??
|
|
953
954
|
(typeof config.queryAware === 'function'
|
|
954
955
|
? config.queryAware(claims)
|
|
955
|
-
: (config.queryAware ?? Boolean(config.
|
|
956
|
+
: (config.queryAware ?? Boolean(config.resolveQueries)));
|
|
956
957
|
const transformVersion = queryAware
|
|
957
958
|
? typeof config.queryTransformVersion === 'function'
|
|
958
959
|
? config.queryTransformVersion(claims)
|
|
@@ -961,7 +962,16 @@ export function createSyncDurableObject(config) {
|
|
|
961
962
|
if (!Number.isSafeInteger(transformVersion) || transformVersion < 0) {
|
|
962
963
|
throw new TypeError('queryTransformVersion must be a non-negative safe integer');
|
|
963
964
|
}
|
|
964
|
-
|
|
965
|
+
// The group lock exists to keep two desired-query patches from being
|
|
966
|
+
// applied out of arrival order when one of them is slow to resolve.
|
|
967
|
+
// A pull carrying no patch applies nothing, and it reads membership
|
|
968
|
+
// inside transactionSync, which is already atomic against whichever
|
|
969
|
+
// patch commits around it. Holding the lock for those pulls only makes
|
|
970
|
+
// an unchanged warm pull wait out somebody else's query-resolution round
|
|
971
|
+
// trip to the application, which is the head-of-line blocking behind
|
|
972
|
+
// the pull tail: a client polls far more often than it changes what it
|
|
973
|
+
// wants, so almost every pull in flight is patch-free.
|
|
974
|
+
const releaseQueryPull = queryAware && config.resolveQueries && body.queries
|
|
965
975
|
? await this.#acquireQueryPullLock(typeof body.clientGroupID === 'string' ? body.clientGroupID : '')
|
|
966
976
|
: null;
|
|
967
977
|
let response;
|
|
@@ -969,43 +979,12 @@ export function createSyncDurableObject(config) {
|
|
|
969
979
|
if (queryAware && body.queries) {
|
|
970
980
|
const queries = body.queries;
|
|
971
981
|
if (Array.isArray(queries.patch)) {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const op = operation;
|
|
979
|
-
if (op.op === 'put') {
|
|
980
|
-
if (!config.resolveQuery || typeof op.name !== 'string') {
|
|
981
|
-
throw requestError('query put requires a server-resolved named query');
|
|
982
|
-
}
|
|
983
|
-
if (!Array.isArray(op.args)) {
|
|
984
|
-
throw requestError('named query args must be an array');
|
|
985
|
-
}
|
|
986
|
-
const args = op.args;
|
|
987
|
-
let ast;
|
|
988
|
-
try {
|
|
989
|
-
// resolveQuery may be async and needs `env` (a consumer can
|
|
990
|
-
// delegate the transform to its app's real synced-queries
|
|
991
|
-
// endpoint over an app service binding — authenticate runs in the
|
|
992
|
-
// worker isolate, but the query loop runs here in the DO, so the
|
|
993
|
-
// binding must come from the DO's own env, not a shared global).
|
|
994
|
-
ast = await config.resolveQuery(op.name, args, claims, this.env);
|
|
995
|
-
}
|
|
996
|
-
catch (error) {
|
|
997
|
-
throw requestError(`unknown or unsupported named query: ${op.name}`);
|
|
998
|
-
}
|
|
999
|
-
patch.push({
|
|
1000
|
-
op: 'put',
|
|
1001
|
-
hash: op.hash,
|
|
1002
|
-
ast,
|
|
1003
|
-
transformVersion,
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
else
|
|
1007
|
-
patch.push(operation);
|
|
1008
|
-
}
|
|
982
|
+
// resolveQueries may be async and needs `env` (a consumer can
|
|
983
|
+
// delegate the transform to its app's real synced-queries
|
|
984
|
+
// endpoint over an app service binding — authenticate runs in the
|
|
985
|
+
// worker isolate, but the query loop runs here in the DO, so the
|
|
986
|
+
// binding must come from the DO's own env, not a shared global).
|
|
987
|
+
const patch = await resolveQueryPatch(queries.patch, (requests) => config.resolveQueries(requests, claims, this.env), transformVersion, requestError);
|
|
1009
988
|
body = { ...body, queries: { ...queries, patch } };
|
|
1010
989
|
}
|
|
1011
990
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { createSyncDurableObject, createSyncWorker } from './host.js';
|
|
2
2
|
export { createQueryCompiler } from './query-compiler.js';
|
|
3
|
+
export { resolveQueryPatch } from './query-patch.js';
|
|
3
4
|
export { visibility } from './types.js';
|
|
4
5
|
export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
|
|
5
|
-
export type { PullCaps, QueryResolver, SyncHostConfig, SyncHostEnv, ServiceBinding, SyncSql, UpstreamConfig, VisibilityConfig, VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from './types.js';
|
|
6
|
+
export type { PullCaps, QueryResolution, QueryResolutionRequest, QueryResolver, SyncHostConfig, SyncHostEnv, ServiceBinding, SyncSql, UpstreamConfig, VisibilityConfig, VisibilityExpression, VisibilityFilter, VisibilityOperand, VisibilityValue, } from './types.js';
|
|
6
7
|
export type { SyncDurableObjectConstructor } from './host.js';
|
|
7
8
|
export type { CompiledTransactionQueryNode, CompiledTransactionQueryPlan, CompiledTransactionQueryRelationship, TransactionQueryBinding, TransactionQueryBudget, TransactionQueryColumn, TransactionQueryColumnType, TransactionQueryExecutionOptions, TransactionQueryWireValue, } from './transaction-query.js';
|
|
8
9
|
export type { TransactionQueryCompiler } from './query-compiler.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createSyncDurableObject, createSyncWorker } from './host.js';
|
|
2
2
|
export { createQueryCompiler } from './query-compiler.js';
|
|
3
|
+
export { resolveQueryPatch } from './query-patch.js';
|
|
3
4
|
export { visibility } from './types.js';
|
|
4
5
|
export { DEFAULT_TRANSACTION_QUERY_BUDGET, TransactionQueryBudgetError, executeTransactionQueryPlan, executeTransactionQueryPlanAsync, } from './transaction-query.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JsonValue } from 'orez-sync-executor';
|
|
2
|
+
export type QueryResolutionRequest = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly args: readonly JsonValue[];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* One resolved query, positionally matched to the request at the same index.
|
|
8
|
+
*
|
|
9
|
+
* An `error` fails only its own query rather than the whole patch, so a client
|
|
10
|
+
* that registers one unknown query alongside nine good ones is told which one
|
|
11
|
+
* is unknown.
|
|
12
|
+
*/
|
|
13
|
+
export type QueryResolution = {
|
|
14
|
+
readonly ast: JsonValue;
|
|
15
|
+
} | {
|
|
16
|
+
readonly error: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Turn a client's desired-query patch into the resolved patch sync-core takes.
|
|
20
|
+
*
|
|
21
|
+
* Both hosts share this because both must agree on exactly one thing: how many
|
|
22
|
+
* times a consumer's resolver is called for one patch. Resolving per query made
|
|
23
|
+
* a pull cost one application round trip per registered query, each one
|
|
24
|
+
* re-authenticating, so a screen mounting eleven views paid eleven of them in
|
|
25
|
+
* series. Everything else here exists to make the single call safe: the patch
|
|
26
|
+
* keeps its order, duplicate (name, args) pairs collapse to one request and fan
|
|
27
|
+
* back out, and a resolver error names the query it came from.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveQueryPatch(patch: readonly unknown[], resolve: (requests: readonly QueryResolutionRequest[]) => readonly QueryResolution[] | Promise<readonly QueryResolution[]>, transformVersion: number, fail: (message: string) => Error): Promise<unknown[]>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a client's desired-query patch into the resolved patch sync-core takes.
|
|
3
|
+
*
|
|
4
|
+
* Both hosts share this because both must agree on exactly one thing: how many
|
|
5
|
+
* times a consumer's resolver is called for one patch. Resolving per query made
|
|
6
|
+
* a pull cost one application round trip per registered query, each one
|
|
7
|
+
* re-authenticating, so a screen mounting eleven views paid eleven of them in
|
|
8
|
+
* series. Everything else here exists to make the single call safe: the patch
|
|
9
|
+
* keeps its order, duplicate (name, args) pairs collapse to one request and fan
|
|
10
|
+
* back out, and a resolver error names the query it came from.
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveQueryPatch(patch, resolve, transformVersion, fail) {
|
|
13
|
+
const puts = [];
|
|
14
|
+
const requests = [];
|
|
15
|
+
const requestByKey = new Map();
|
|
16
|
+
patch.forEach((operation, index) => {
|
|
17
|
+
if (!operation || typeof operation !== 'object')
|
|
18
|
+
return;
|
|
19
|
+
const op = operation;
|
|
20
|
+
if (op.op !== 'put')
|
|
21
|
+
return;
|
|
22
|
+
if (typeof op.name !== 'string') {
|
|
23
|
+
throw fail('query put requires a server-resolved named query');
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(op.args))
|
|
26
|
+
throw fail('named query args must be an array');
|
|
27
|
+
const key = JSON.stringify([op.name, op.args]);
|
|
28
|
+
if (!requestByKey.has(key)) {
|
|
29
|
+
requestByKey.set(key, requests.length);
|
|
30
|
+
requests.push({ name: op.name, args: op.args });
|
|
31
|
+
}
|
|
32
|
+
puts.push({ index, key, name: op.name, hash: op.hash });
|
|
33
|
+
});
|
|
34
|
+
if (puts.length === 0)
|
|
35
|
+
return [...patch];
|
|
36
|
+
const resolved = await resolve(requests);
|
|
37
|
+
if (resolved.length !== requests.length) {
|
|
38
|
+
throw new Error(`query resolver returned ${resolved.length} results for ${requests.length} queries`);
|
|
39
|
+
}
|
|
40
|
+
const result = [...patch];
|
|
41
|
+
for (const put of puts) {
|
|
42
|
+
const resolution = resolved[requestByKey.get(put.key)];
|
|
43
|
+
if (!resolution || 'error' in resolution) {
|
|
44
|
+
throw fail(`unknown or unsupported named query: ${put.name}`);
|
|
45
|
+
}
|
|
46
|
+
result[put.index] = {
|
|
47
|
+
op: 'put',
|
|
48
|
+
hash: put.hash,
|
|
49
|
+
ast: resolution.ast,
|
|
50
|
+
transformVersion,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
import type { QueryResolution, QueryResolutionRequest } from './query-patch.js';
|
|
1
2
|
import type { TransactionQueryBudget } from './transaction-query.js';
|
|
2
3
|
import type { Schema } from '@rocicorp/zero';
|
|
3
|
-
import type { ExecResult,
|
|
4
|
+
import type { ExecResult, MutatorRegistry, NormalizedClaims, SqlStatementMetadata, VisibilityConfig } from 'orez-sync-executor';
|
|
4
5
|
export { visibility, type VisibilityExpression, type VisibilityFilter, type VisibilityOperand, type VisibilityValue, } from './visibility.js';
|
|
5
6
|
export type { VisibilityConfig } from 'orez-sync-executor';
|
|
7
|
+
export type { QueryResolution, QueryResolutionRequest } from './query-patch.js';
|
|
6
8
|
export interface SyncSql {
|
|
7
9
|
exec(sql: string, params?: readonly unknown[], metadata?: SqlStatementMetadata): ExecResult;
|
|
8
10
|
query<Row extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Row[];
|
|
9
11
|
}
|
|
10
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a whole desired-query patch in one call.
|
|
14
|
+
*
|
|
15
|
+
* This is a batch contract because it is nearly always a network call: a
|
|
16
|
+
* consumer delegates the transform to its application, which authenticates and
|
|
17
|
+
* answers over a service binding. Resolving one query per call made a pull's
|
|
18
|
+
* cost linear in the number of queries a screen registers, and each round trip
|
|
19
|
+
* re-authenticated. Measured against production with a captured mobile client's
|
|
20
|
+
* desired-query set: an 11-query pull took 23.3 s against 2.0 s for the same
|
|
21
|
+
* pull carrying no queries.
|
|
22
|
+
*
|
|
23
|
+
* Implementations must return exactly one entry per request, in request order.
|
|
24
|
+
*/
|
|
25
|
+
export type QueryResolver = (requests: readonly QueryResolutionRequest[], claims: NormalizedClaims, env: SyncHostEnv) => readonly QueryResolution[] | Promise<readonly QueryResolution[]>;
|
|
11
26
|
export type PullCaps = {
|
|
12
27
|
maxChangeRows: number;
|
|
13
28
|
maxChangeBytes: number;
|
|
@@ -85,7 +100,8 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
|
|
|
85
100
|
/** Enable desired-query pulls for this namespace and resolve named queries
|
|
86
101
|
* into validated Zero ASTs before they reach sync-core. */
|
|
87
102
|
queryAware?: boolean | ((claims: NormalizedClaims) => boolean);
|
|
88
|
-
|
|
103
|
+
/** Resolve every named query in one desired-query patch, in one call. */
|
|
104
|
+
resolveQueries?: QueryResolver;
|
|
89
105
|
/** Server-owned invalidation epoch for permission/schema transforms. */
|
|
90
106
|
queryTransformVersion?: number | ((claims: NormalizedClaims) => number);
|
|
91
107
|
/** Enable consumer visibility from the first request. Defaults to false for harnesses. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez-sync-cf-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/query-compiler.d.ts",
|
|
20
20
|
"import": "./dist/query-compiler.js"
|
|
21
21
|
},
|
|
22
|
+
"./query-patch": {
|
|
23
|
+
"types": "./dist/query-patch.d.ts",
|
|
24
|
+
"import": "./dist/query-patch.js"
|
|
25
|
+
},
|
|
22
26
|
"./transaction-query": {
|
|
23
27
|
"types": "./dist/transaction-query.d.ts",
|
|
24
28
|
"import": "./dist/transaction-query.js"
|
|
@@ -56,7 +60,7 @@
|
|
|
56
60
|
"typecheck": "bun run build:dist:platform && tsc --noEmit"
|
|
57
61
|
},
|
|
58
62
|
"dependencies": {
|
|
59
|
-
"orez-sync-executor": "0.
|
|
63
|
+
"orez-sync-executor": "0.11.0"
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
66
|
"@cloudflare/workers-types": "4.20260617.1",
|