orez-sync-cf-host 0.16.19 → 0.16.20
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 +6 -0
- package/dist/generated/sync_wasm.d.ts +9 -0
- package/dist/generated/sync_wasm.js +20 -0
- package/dist/generated/sync_wasm_bg.js +20 -0
- package/dist/generated/sync_wasm_bg.wasm +0 -0
- package/dist/host.js +130 -15
- package/dist/types.d.ts +9 -0
- package/dist/wasm.d.ts +1 -0
- package/dist/wasm.js +1 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -144,6 +144,12 @@ Pass a mint callback to the low-level HTTP transport. It calls `getToken()` for
|
|
|
144
144
|
every socket attempt, including reconnects, so short-lived tokens are never
|
|
145
145
|
reused after the wake connection drops:
|
|
146
146
|
|
|
147
|
+
Calling `ensureHttpPullTransport` again with a custom token provider replaces
|
|
148
|
+
that provider for future wake attempts while retaining the transport and
|
|
149
|
+
WebSocket shim. Module reloads can recreate the callback without chaining
|
|
150
|
+
transports. Switching between custom minting, auth-token wake and disabled wake,
|
|
151
|
+
or changing transport routing or timing, remains a configuration conflict.
|
|
152
|
+
|
|
147
153
|
```ts
|
|
148
154
|
import { ensureHttpPullTransport } from 'orez-lite/client'
|
|
149
155
|
|
|
@@ -115,6 +115,14 @@ export function engine_state(db: any): any;
|
|
|
115
115
|
|
|
116
116
|
export function engine_version(): string;
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The clients a wake after an ingest should reach: every client whose group
|
|
120
|
+
* can see a row committed after `since`. `{ all: true }` means wake every
|
|
121
|
+
* socket; otherwise `clientIDs` lists the clients to wake. Call it with a
|
|
122
|
+
* transaction open, after the ingest that advanced past `since`.
|
|
123
|
+
*/
|
|
124
|
+
export function engine_wake_targets(db: any, schema: any, since: string): any;
|
|
125
|
+
|
|
118
126
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
119
127
|
|
|
120
128
|
export interface InitOutput {
|
|
@@ -142,6 +150,7 @@ export interface InitOutput {
|
|
|
142
150
|
readonly engine_schema_revision: () => [number, number];
|
|
143
151
|
readonly engine_state: (a: any) => [number, number, number];
|
|
144
152
|
readonly engine_version: () => [number, number];
|
|
153
|
+
readonly engine_wake_targets: (a: any, b: any, c: number, d: number) => [number, number, number];
|
|
145
154
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
146
155
|
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
147
156
|
readonly __wbindgen_exn_store: (a: number) => void;
|
|
@@ -397,6 +397,26 @@ export function engine_version() {
|
|
|
397
397
|
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The clients a wake after an ingest should reach: every client whose group
|
|
403
|
+
* can see a row committed after `since`. `{ all: true }` means wake every
|
|
404
|
+
* socket; otherwise `clientIDs` lists the clients to wake. Call it with a
|
|
405
|
+
* transaction open, after the ingest that advanced past `since`.
|
|
406
|
+
* @param {any} db
|
|
407
|
+
* @param {any} schema
|
|
408
|
+
* @param {string} since
|
|
409
|
+
* @returns {any}
|
|
410
|
+
*/
|
|
411
|
+
export function engine_wake_targets(db, schema, since) {
|
|
412
|
+
const ptr0 = passStringToWasm0(since, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
413
|
+
const len0 = WASM_VECTOR_LEN;
|
|
414
|
+
const ret = wasm.engine_wake_targets(db, schema, ptr0, len0);
|
|
415
|
+
if (ret[2]) {
|
|
416
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
417
|
+
}
|
|
418
|
+
return takeFromExternrefTable0(ret[0]);
|
|
419
|
+
}
|
|
400
420
|
function __wbg_get_imports() {
|
|
401
421
|
const import0 = {
|
|
402
422
|
__proto__: null,
|
|
@@ -395,6 +395,26 @@ export function engine_version() {
|
|
|
395
395
|
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The clients a wake after an ingest should reach: every client whose group
|
|
401
|
+
* can see a row committed after `since`. `{ all: true }` means wake every
|
|
402
|
+
* socket; otherwise `clientIDs` lists the clients to wake. Call it with a
|
|
403
|
+
* transaction open, after the ingest that advanced past `since`.
|
|
404
|
+
* @param {any} db
|
|
405
|
+
* @param {any} schema
|
|
406
|
+
* @param {string} since
|
|
407
|
+
* @returns {any}
|
|
408
|
+
*/
|
|
409
|
+
export function engine_wake_targets(db, schema, since) {
|
|
410
|
+
const ptr0 = passStringToWasm0(since, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
411
|
+
const len0 = WASM_VECTOR_LEN;
|
|
412
|
+
const ret = wasm.engine_wake_targets(db, schema, ptr0, len0);
|
|
413
|
+
if (ret[2]) {
|
|
414
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
415
|
+
}
|
|
416
|
+
return takeFromExternrefTable0(ret[0]);
|
|
417
|
+
}
|
|
398
418
|
export function __wbg_Error_92b29b0548f8b746(arg0, arg1) {
|
|
399
419
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
400
420
|
return ret;
|
|
Binary file
|
package/dist/host.js
CHANGED
|
@@ -7,7 +7,7 @@ import { resolveQueryPatch } from './query-patch.js';
|
|
|
7
7
|
import { ServingLagTracker } from './serving-lag.js';
|
|
8
8
|
import { decodeSqlParams, SqlStorageDirect, SqlStorageMutatorTransaction, SqlStorageSyncDb, } from './sql-storage-adapter.js';
|
|
9
9
|
import { DEFAULT_UPSTREAM_MAX_RESPONSE_BYTES, DEFAULT_UPSTREAM_MAX_REQUEST_BYTES, DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS, fetchBoundedUpstreamJson, readBoundedJsonResponse, readBoundedStream, UpstreamResponseLimitError, } from './upstream-response.js';
|
|
10
|
-
import { engine_authorize_realtime_subscription, engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, 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_schema_revision, engine_state, engine_version, } from './wasm.js';
|
|
10
|
+
import { engine_authorize_realtime_subscription, engine_apply_snapshot_changes, engine_apply_snapshot_page, engine_apply_upstream, engine_begin_snapshot_generation, engine_finalize, engine_finalize_snapshot_generation, 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_schema_revision, engine_state, engine_version, engine_wake_targets, } from './wasm.js';
|
|
11
11
|
import { IngestBreakerError, IngestCircuitBreaker, retryDelayMs, shouldRetryDelegatedPush, } from './write-safeguards.js';
|
|
12
12
|
const NAMESPACE_HEADER = 'x-orez-sync-namespace';
|
|
13
13
|
const UPSTREAM_PATH_HEADER = 'x-orez-sync-upstream-path';
|
|
@@ -18,6 +18,7 @@ const WAKE_SUBSCRIBER_TAG = 'orez:wake-subscriber';
|
|
|
18
18
|
// the worker always deletes from the incoming request before setting its own,
|
|
19
19
|
// so a client cannot present one.
|
|
20
20
|
const IDENTITY_HEADER = 'x-orez-sync-identity';
|
|
21
|
+
const WAKE_ISSUED_AT_HEADER = 'x-orez-wake-issued-at';
|
|
21
22
|
const DEFAULT_SNAPSHOT_PAGE_ROWS = 2_000;
|
|
22
23
|
const MIN_SNAPSHOT_PAGE_ROWS = 100;
|
|
23
24
|
const WORKER_STAGE_TELEMETRY_SAMPLE_RATE = 0.01;
|
|
@@ -51,6 +52,14 @@ function upstreamBatchTables(batch) {
|
|
|
51
52
|
}
|
|
52
53
|
return tables;
|
|
53
54
|
}
|
|
55
|
+
function isRealtimeIdentity(identity) {
|
|
56
|
+
return (typeof identity === 'object' &&
|
|
57
|
+
identity !== null &&
|
|
58
|
+
typeof identity.userID === 'string' &&
|
|
59
|
+
typeof identity.clientID === 'string' &&
|
|
60
|
+
typeof identity.clientGroupID === 'string' &&
|
|
61
|
+
identity.clientGroupID.length > 0);
|
|
62
|
+
}
|
|
54
63
|
function freshCounters() {
|
|
55
64
|
return {
|
|
56
65
|
pulls: 0,
|
|
@@ -167,6 +176,14 @@ async function boundedRequestObject(request, maxBytes, timeoutMs) {
|
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
function routeAfterNamespace(pathname) {
|
|
179
|
+
if (pathname.startsWith('/admin/') ||
|
|
180
|
+
pathname === '/wake' ||
|
|
181
|
+
pathname === '/notify' ||
|
|
182
|
+
pathname === '/pull' ||
|
|
183
|
+
pathname === '/push' ||
|
|
184
|
+
pathname.startsWith('/realtime/')) {
|
|
185
|
+
return pathname;
|
|
186
|
+
}
|
|
170
187
|
const [, , ...parts] = pathname.split('/');
|
|
171
188
|
return `/${parts.join('/')}`;
|
|
172
189
|
}
|
|
@@ -324,6 +341,7 @@ export function createSyncWorker(config) {
|
|
|
324
341
|
const sampled = Math.random() < WORKER_STAGE_TELEMETRY_SAMPLE_RATE;
|
|
325
342
|
const isAdmin = route.startsWith('/admin/');
|
|
326
343
|
let wakeUserID = null;
|
|
344
|
+
let wakeIssuedAt = null;
|
|
327
345
|
if (route === '/wake') {
|
|
328
346
|
let wakeRequest = request;
|
|
329
347
|
const protocol = request.headers.get('sec-websocket-protocol')?.trim();
|
|
@@ -350,8 +368,13 @@ export function createSyncWorker(config) {
|
|
|
350
368
|
const wake = await config.authorizeWake(wakeRequest, env);
|
|
351
369
|
if (!wake)
|
|
352
370
|
return json({ error: 'missing wake capability' }, 401);
|
|
353
|
-
if (typeof wake === 'object')
|
|
371
|
+
if (typeof wake === 'object') {
|
|
354
372
|
wakeUserID = wake.userID;
|
|
373
|
+
if (typeof wake.tokenIssuedAt === 'number' &&
|
|
374
|
+
Number.isFinite(wake.tokenIssuedAt)) {
|
|
375
|
+
wakeIssuedAt = wake.tokenIssuedAt;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
355
378
|
// A namespace that streams fields authorizes every subscription against
|
|
356
379
|
// this userID, so a capability that does not carry one cannot open the
|
|
357
380
|
// socket. Failing here names the cause; accepting it would produce a
|
|
@@ -377,6 +400,7 @@ export function createSyncWorker(config) {
|
|
|
377
400
|
headers.delete(NAMESPACE_HEADER);
|
|
378
401
|
headers.delete(UPSTREAM_PATH_HEADER);
|
|
379
402
|
headers.delete(IDENTITY_HEADER);
|
|
403
|
+
headers.delete(WAKE_ISSUED_AT_HEADER);
|
|
380
404
|
headers.delete(NOTIFY_IF_SUBSCRIBED_HEADER);
|
|
381
405
|
let forwardedBody = null;
|
|
382
406
|
// /wake and /realtime/produce are both websocket upgrades and have no
|
|
@@ -432,6 +456,8 @@ export function createSyncWorker(config) {
|
|
|
432
456
|
// nothing gained by moving them here.
|
|
433
457
|
if (wakeUserID)
|
|
434
458
|
headers.set(IDENTITY_HEADER, encodeURIComponent(wakeUserID));
|
|
459
|
+
if (wakeIssuedAt !== null)
|
|
460
|
+
headers.set(WAKE_ISSUED_AT_HEADER, String(wakeIssuedAt));
|
|
435
461
|
const hashedNamespace = await namespaceHash(namespace);
|
|
436
462
|
headers.set(NAMESPACE_HEADER, hashedNamespace);
|
|
437
463
|
try {
|
|
@@ -480,7 +506,7 @@ export function createSyncDurableObject(config) {
|
|
|
480
506
|
validateSyncHostConfig(config);
|
|
481
507
|
const compileQuery = createQueryCompiler(config.schema);
|
|
482
508
|
const defaultRetainChanges = String(config.retainChanges ?? 4_096);
|
|
483
|
-
const idleTeardownMs = config.idleTeardownMs ??
|
|
509
|
+
const idleTeardownMs = config.idleTeardownMs ?? null;
|
|
484
510
|
// A CF fan-out wakes every client into an HTTP pull. Give concurrent writer
|
|
485
511
|
// requests a real batching window so a storm burst creates one pull wave.
|
|
486
512
|
const wakeCoalesceMs = config.wakeCoalesceMs ?? 25;
|
|
@@ -522,6 +548,7 @@ export function createSyncDurableObject(config) {
|
|
|
522
548
|
#wakeOrigins = new Set();
|
|
523
549
|
#wakeRecipients = new Set();
|
|
524
550
|
#wakeTables = new Set();
|
|
551
|
+
#wakeUntargeted = false;
|
|
525
552
|
#wakePromise = null;
|
|
526
553
|
#lastWake = null;
|
|
527
554
|
// Streaming fields. Null when the namespace configures no manifest, which
|
|
@@ -543,6 +570,7 @@ export function createSyncDurableObject(config) {
|
|
|
543
570
|
});
|
|
544
571
|
constructor(ctx, env) {
|
|
545
572
|
super(ctx, env);
|
|
573
|
+
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong'));
|
|
546
574
|
const recordRowsWritten = (rows) => {
|
|
547
575
|
this.#sqlBilling.rowsWritten += rows;
|
|
548
576
|
if (this.#recordingIngestBillable)
|
|
@@ -644,6 +672,13 @@ export function createSyncDurableObject(config) {
|
|
|
644
672
|
]);
|
|
645
673
|
this.#initSkipped = this.#controlGet('initFingerprint') === initFingerprint;
|
|
646
674
|
if (!this.#initSkipped) {
|
|
675
|
+
this.#directSql.exec(`CREATE TABLE IF NOT EXISTS orez_revocations (
|
|
676
|
+
user_id TEXT NOT NULL,
|
|
677
|
+
revoked_at INTEGER NOT NULL
|
|
678
|
+
)`);
|
|
679
|
+
this.#directSql.exec('CREATE INDEX IF NOT EXISTS idx_orez_revocations_user_id ON orez_revocations(user_id, revoked_at)');
|
|
680
|
+
this.#directSql.exec('CREATE VIEW IF NOT EXISTS revocations AS SELECT user_id, revoked_at FROM orez_revocations');
|
|
681
|
+
this.#directSql.exec('CREATE TRIGGER IF NOT EXISTS trg_revocations_insert INSTEAD OF INSERT ON revocations BEGIN INSERT INTO orez_revocations(user_id, revoked_at) VALUES (NEW.user_id, NEW.revoked_at); END;');
|
|
647
682
|
config.initialize(this.#directSql);
|
|
648
683
|
this.#directSql.exec("INSERT OR IGNORE INTO _zsync_host_control (key, value) VALUES ('writerEnabled', '1')");
|
|
649
684
|
this.#wasm(() => engine_init_schema(this.#engineDb, config.schema));
|
|
@@ -673,6 +708,8 @@ export function createSyncDurableObject(config) {
|
|
|
673
708
|
return call();
|
|
674
709
|
}
|
|
675
710
|
#simulateIdleTeardown(now) {
|
|
711
|
+
if (idleTeardownMs === null)
|
|
712
|
+
return;
|
|
676
713
|
if (this.#lastRequestAt > 0 && now - this.#lastRequestAt >= idleTeardownMs) {
|
|
677
714
|
this.#bootID = crypto.randomUUID();
|
|
678
715
|
this.#hibernations++;
|
|
@@ -682,6 +719,7 @@ export function createSyncDurableObject(config) {
|
|
|
682
719
|
this.#wakeOrigins.clear();
|
|
683
720
|
this.#wakeRecipients.clear();
|
|
684
721
|
this.#wakeTables.clear();
|
|
722
|
+
this.#wakeUntargeted = false;
|
|
685
723
|
this.#wakePromise = null;
|
|
686
724
|
this.#lastWake = null;
|
|
687
725
|
// Real hibernation reconstructs the object, so the hub and every
|
|
@@ -1226,7 +1264,10 @@ export function createSyncDurableObject(config) {
|
|
|
1226
1264
|
}
|
|
1227
1265
|
}
|
|
1228
1266
|
if (snapshotCompleted || total > 0 || endingWatermark !== startingWatermark) {
|
|
1229
|
-
|
|
1267
|
+
// a completed snapshot replaced the replica wholesale: every client
|
|
1268
|
+
// re-pulls. otherwise wake only the clients these rows can reach.
|
|
1269
|
+
const targets = snapshotCompleted ? null : this.#wakeTargets(startingWatermark);
|
|
1270
|
+
await this.#enqueueWake('__upstream__', changedTables, targets);
|
|
1230
1271
|
}
|
|
1231
1272
|
return total;
|
|
1232
1273
|
})().finally(() => {
|
|
@@ -1302,11 +1343,33 @@ export function createSyncDurableObject(config) {
|
|
|
1302
1343
|
...fields,
|
|
1303
1344
|
}));
|
|
1304
1345
|
}
|
|
1305
|
-
|
|
1346
|
+
// the clients a batch committed after `since` can reach, or null for every
|
|
1347
|
+
// socket. a wake is advisory, so a targeting failure wakes everyone rather
|
|
1348
|
+
// than failing the ingest that serves the pull waiting on it.
|
|
1349
|
+
#wakeTargets(since) {
|
|
1350
|
+
try {
|
|
1351
|
+
const targets = this.ctx.storage.transactionSync(() => this.#wasm(() => engine_wake_targets(this.#engineDb, config.schema, since)));
|
|
1352
|
+
return targets.all ? null : new Set(targets.clientIDs);
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
console.error(JSON.stringify({
|
|
1356
|
+
event: 'sync_wake_target_error',
|
|
1357
|
+
hostVersion: config.hostVersion,
|
|
1358
|
+
error: errorMessage(error),
|
|
1359
|
+
}));
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
#enqueueWake(originClientID, tables = [], targets = null) {
|
|
1306
1364
|
this.#wakeOrigins.add(originClientID);
|
|
1307
1365
|
for (const table of tables)
|
|
1308
1366
|
this.#wakeTables.add(table);
|
|
1309
|
-
|
|
1367
|
+
if (targets === null)
|
|
1368
|
+
this.#wakeUntargeted = true;
|
|
1369
|
+
const sockets = targets === null
|
|
1370
|
+
? this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG)
|
|
1371
|
+
: [...targets].flatMap((clientID) => this.ctx.getWebSockets(`client:${clientID}`));
|
|
1372
|
+
for (const socket of sockets) {
|
|
1310
1373
|
const attachment = socketAttachment(socket);
|
|
1311
1374
|
if (!attachment ||
|
|
1312
1375
|
attachment.clientID === originClientID ||
|
|
@@ -1329,6 +1392,8 @@ export function createSyncDurableObject(config) {
|
|
|
1329
1392
|
this.#wakeRecipients = new Set();
|
|
1330
1393
|
const tables = [...this.#wakeTables].sort();
|
|
1331
1394
|
this.#wakeTables = new Set();
|
|
1395
|
+
const targeted = !this.#wakeUntargeted;
|
|
1396
|
+
this.#wakeUntargeted = false;
|
|
1332
1397
|
this.#counters.wakeBatches++;
|
|
1333
1398
|
let sent = 0;
|
|
1334
1399
|
const sockets = this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG);
|
|
@@ -1346,6 +1411,7 @@ export function createSyncDurableObject(config) {
|
|
|
1346
1411
|
const wakeStatus = {
|
|
1347
1412
|
at: Date.now(),
|
|
1348
1413
|
tables,
|
|
1414
|
+
targeted,
|
|
1349
1415
|
socketCount: sockets.length,
|
|
1350
1416
|
originCount: origins.size,
|
|
1351
1417
|
sent,
|
|
@@ -1724,7 +1790,7 @@ export function createSyncDurableObject(config) {
|
|
|
1724
1790
|
this.#realtimeConnections.set(socket, host.acceptProducer(socket, attachment.producerID));
|
|
1725
1791
|
continue;
|
|
1726
1792
|
}
|
|
1727
|
-
if (!attachment.identity)
|
|
1793
|
+
if (!isRealtimeIdentity(attachment.identity))
|
|
1728
1794
|
continue;
|
|
1729
1795
|
subscribers.push({
|
|
1730
1796
|
socket,
|
|
@@ -1760,19 +1826,31 @@ export function createSyncDurableObject(config) {
|
|
|
1760
1826
|
const clientID = params.get('clientID');
|
|
1761
1827
|
if (!clientID)
|
|
1762
1828
|
return json({ error: 'clientID is required' }, 400);
|
|
1829
|
+
const encodedUserID = request.headers.get(IDENTITY_HEADER);
|
|
1830
|
+
const rawIssuedAt = request.headers.get(WAKE_ISSUED_AT_HEADER);
|
|
1831
|
+
const userID = encodedUserID ? decodeURIComponent(encodedUserID) : null;
|
|
1832
|
+
if (userID) {
|
|
1833
|
+
const rows = this.#directSql.query('SELECT MAX(revoked_at) AS revoked_at FROM orez_revocations WHERE user_id = ?', [userID]);
|
|
1834
|
+
const latestRevocation = rows[0]?.revoked_at;
|
|
1835
|
+
if (typeof latestRevocation === 'number' && Number.isFinite(latestRevocation)) {
|
|
1836
|
+
const tokenIssuedAt = rawIssuedAt !== null ? Number(rawIssuedAt) : -Infinity;
|
|
1837
|
+
if (Number.isNaN(tokenIssuedAt) || tokenIssuedAt < latestRevocation) {
|
|
1838
|
+
return json({ error: 'revoked wake capability' }, 401);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1763
1842
|
const pair = new WebSocketPair();
|
|
1764
1843
|
const [client, server] = Object.values(pair);
|
|
1765
1844
|
// The userID is the worker's, taken from the authenticated request. The
|
|
1766
1845
|
// group is the client's own claim, and stays a claim: every subscription
|
|
1767
1846
|
// is checked against this userID before a row is read, so asserting
|
|
1768
1847
|
// someone else's group buys nothing.
|
|
1769
|
-
const encodedUserID = request.headers.get(IDENTITY_HEADER);
|
|
1770
1848
|
const clientGroupID = params.get('clientGroupID');
|
|
1771
|
-
const identity = encodedUserID
|
|
1849
|
+
const identity = encodedUserID
|
|
1772
1850
|
? {
|
|
1773
1851
|
userID: decodeURIComponent(encodedUserID),
|
|
1774
1852
|
clientID,
|
|
1775
|
-
clientGroupID,
|
|
1853
|
+
...(clientGroupID ? { clientGroupID } : {}),
|
|
1776
1854
|
}
|
|
1777
1855
|
: undefined;
|
|
1778
1856
|
server.serializeAttachment({
|
|
@@ -1780,7 +1858,7 @@ export function createSyncDurableObject(config) {
|
|
|
1780
1858
|
identity,
|
|
1781
1859
|
});
|
|
1782
1860
|
this.ctx.acceptWebSocket(server, [WAKE_SUBSCRIBER_TAG, `client:${clientID}`]);
|
|
1783
|
-
if (identity) {
|
|
1861
|
+
if (isRealtimeIdentity(identity)) {
|
|
1784
1862
|
const host = this.#realtimeHost();
|
|
1785
1863
|
if (host) {
|
|
1786
1864
|
this.#realtimeConnections.set(server, host.acceptSubscriber(server, identity, clientID));
|
|
@@ -1802,6 +1880,47 @@ export function createSyncDurableObject(config) {
|
|
|
1802
1880
|
#admin(route, request, upstreamPath) {
|
|
1803
1881
|
if (route === '/admin/health')
|
|
1804
1882
|
return json({ ok: true });
|
|
1883
|
+
if (route === '/admin/revoke') {
|
|
1884
|
+
if (request.method !== 'POST') {
|
|
1885
|
+
return json({ error: 'method not allowed' }, 405);
|
|
1886
|
+
}
|
|
1887
|
+
return request
|
|
1888
|
+
.json()
|
|
1889
|
+
.catch(() => ({}))
|
|
1890
|
+
.then((body) => {
|
|
1891
|
+
const parsed = typeof body === 'object' && body !== null ? body : {};
|
|
1892
|
+
const targetUserID = 'userID' in parsed && typeof parsed.userID === 'string'
|
|
1893
|
+
? parsed.userID
|
|
1894
|
+
: 'userId' in parsed && typeof parsed.userId === 'string'
|
|
1895
|
+
? parsed.userId
|
|
1896
|
+
: null;
|
|
1897
|
+
if (!targetUserID) {
|
|
1898
|
+
return json({ error: 'userID is required' }, 400);
|
|
1899
|
+
}
|
|
1900
|
+
const revokedAt = 'now' in parsed &&
|
|
1901
|
+
typeof parsed.now === 'number' &&
|
|
1902
|
+
Number.isFinite(parsed.now)
|
|
1903
|
+
? parsed.now
|
|
1904
|
+
: Date.now();
|
|
1905
|
+
this.ctx.storage.transactionSync(() => {
|
|
1906
|
+
this.#directSql.exec(`CREATE TABLE IF NOT EXISTS orez_revocations (
|
|
1907
|
+
user_id TEXT NOT NULL,
|
|
1908
|
+
revoked_at INTEGER NOT NULL
|
|
1909
|
+
)`);
|
|
1910
|
+
this.#directSql.exec('INSERT INTO orez_revocations (user_id, revoked_at) VALUES (?, ?)', [targetUserID, revokedAt]);
|
|
1911
|
+
});
|
|
1912
|
+
let closedSockets = 0;
|
|
1913
|
+
for (const socket of this.ctx.getWebSockets(WAKE_SUBSCRIBER_TAG)) {
|
|
1914
|
+
const attachment = socketAttachment(socket);
|
|
1915
|
+
if (attachment?.identity?.userID === targetUserID) {
|
|
1916
|
+
this.#realtimeDrop(socket);
|
|
1917
|
+
socketCloseQuietly(socket, 1008, 'user revoked');
|
|
1918
|
+
closedSockets++;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return json({ ok: true, userID: targetUserID, revokedAt, closedSockets });
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1805
1924
|
if (route === '/admin/sql-billing')
|
|
1806
1925
|
return json({ ...this.#sqlBilling });
|
|
1807
1926
|
if (route === '/admin/upstream-write-budget' && request.method === 'GET')
|
|
@@ -2061,10 +2180,6 @@ export function createSyncDurableObject(config) {
|
|
|
2061
2180
|
return new Response(null, { status: 101, webSocket: client });
|
|
2062
2181
|
}
|
|
2063
2182
|
async webSocketMessage(socket, message) {
|
|
2064
|
-
if (message === 'ping') {
|
|
2065
|
-
socket.send('pong');
|
|
2066
|
-
return;
|
|
2067
|
-
}
|
|
2068
2183
|
this.#initialize();
|
|
2069
2184
|
const host = this.#realtimeHost();
|
|
2070
2185
|
if (!host || typeof message !== 'string')
|
package/dist/types.d.ts
CHANGED
|
@@ -91,8 +91,10 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
|
|
|
91
91
|
* never arrives. */
|
|
92
92
|
authorizeWake(request: Request, env: Env): boolean | {
|
|
93
93
|
userID: string;
|
|
94
|
+
tokenIssuedAt?: number;
|
|
94
95
|
} | Promise<boolean | {
|
|
95
96
|
userID: string;
|
|
97
|
+
tokenIssuedAt?: number;
|
|
96
98
|
}>;
|
|
97
99
|
/** Authorize upstream service notifications before selecting a namespace DO. */
|
|
98
100
|
authorizeNotify(request: Request, env: Env): boolean | Promise<boolean>;
|
|
@@ -143,6 +145,13 @@ export type SyncHostConfig<Env extends SyncHostEnv = SyncHostEnv, S extends Sche
|
|
|
143
145
|
/** Server-owned invalidation epoch for permission/schema transforms. */
|
|
144
146
|
queryTransformVersion?: number | ((claims: NormalizedClaims) => number);
|
|
145
147
|
retainChanges?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Test model of a hibernation eviction: after this much inactivity the next
|
|
150
|
+
* request resets the object's in-memory state the way a real eviction would.
|
|
151
|
+
* Leave it unset in production, where the runtime evicts for real; a
|
|
152
|
+
* simulated reset there drops pending wakes whenever one request blocks the
|
|
153
|
+
* object for longer than the window.
|
|
154
|
+
*/
|
|
146
155
|
idleTeardownMs?: number;
|
|
147
156
|
wakeCoalesceMs?: number;
|
|
148
157
|
/** per-query guard for recursive transaction query materialization. */
|
package/dist/wasm.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export declare const engine_record_app_error: (db: any, client_group_id: string,
|
|
|
21
21
|
export declare const engine_schema_revision: () => string;
|
|
22
22
|
export declare const engine_state: (db: any) => any;
|
|
23
23
|
export declare const engine_version: () => string;
|
|
24
|
+
export declare const engine_wake_targets: (db: any, schema: any, since: string) => any;
|
package/dist/wasm.js
CHANGED
|
@@ -38,3 +38,4 @@ export const engine_record_app_error = withWasm(generated.engine_record_app_erro
|
|
|
38
38
|
export const engine_schema_revision = withWasm(generated.engine_schema_revision);
|
|
39
39
|
export const engine_state = withWasm(generated.engine_state);
|
|
40
40
|
export const engine_version = withWasm(generated.engine_version);
|
|
41
|
+
export const engine_wake_targets = withWasm(generated.engine_wake_targets);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez-sync-cf-host",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.20",
|
|
4
4
|
"description": "Internal orez Cloudflare sync host. Do not import directly: consumers use orez-lite/cloudflare/sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@opentelemetry/api": "^1.9.0",
|
|
63
|
-
"orez-sync-executor": "0.16.
|
|
63
|
+
"orez-sync-executor": "0.16.20"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@cloudflare/workers-types": "4.20260617.1",
|
|
@@ -87,5 +87,6 @@
|
|
|
87
87
|
"type": "git",
|
|
88
88
|
"url": "git+https://github.com/natew/orez.git",
|
|
89
89
|
"directory": "packages/sync-cf-host"
|
|
90
|
-
}
|
|
90
|
+
},
|
|
91
|
+
"releaseSourceCommit": "ea248201d35f2c30b74a6401eda49b9bdd3d2287"
|
|
91
92
|
}
|