orez 0.5.9 → 0.5.12
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/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/cf-do/tx-journal.d.ts +6 -5
- package/dist/cf-do/tx-journal.d.ts.map +1 -1
- package/dist/cf-do/tx-journal.js +116 -27
- package/dist/cf-do/tx-journal.js.map +1 -1
- package/dist/pg-proxy-browser.d.ts +3 -1
- package/dist/pg-proxy-browser.d.ts.map +1 -1
- package/dist/pg-proxy-browser.js +240 -210
- package/dist/pg-proxy-browser.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +8 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +27 -9
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-sqlite-compiler/compiler.d.ts +17 -0
- package/dist/pg-sqlite-compiler/compiler.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/compiler.js +80 -0
- package/dist/pg-sqlite-compiler/compiler.js.map +1 -0
- package/dist/pg-sqlite-compiler/index.d.ts +1 -16
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -1
- package/dist/pg-sqlite-compiler/index.js +2 -79
- package/dist/pg-sqlite-compiler/index.js.map +1 -1
- package/dist/replication/handler.d.ts +8 -7
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +147 -116
- package/dist/replication/handler.js.map +1 -1
- package/dist/worker/browser-build-config.d.ts.map +1 -1
- package/dist/worker/browser-build-config.js +1 -0
- package/dist/worker/browser-build-config.js.map +1 -1
- package/dist/worker/browser-embed.d.ts +3 -2
- package/dist/worker/browser-embed.d.ts.map +1 -1
- package/dist/worker/browser-embed.js +187 -150
- package/dist/worker/browser-embed.js.map +1 -1
- package/dist/worker/cf-instance-runtime.d.ts +62 -0
- package/dist/worker/cf-instance-runtime.d.ts.map +1 -0
- package/dist/worker/cf-instance-runtime.js +260 -0
- package/dist/worker/cf-instance-runtime.js.map +1 -0
- package/dist/worker/cf-patches.d.ts +9 -6
- package/dist/worker/cf-patches.d.ts.map +1 -1
- package/dist/worker/cf-patches.js +117 -60
- package/dist/worker/cf-patches.js.map +1 -1
- package/dist/worker/durable-object-websocket-handoff.d.ts +2 -0
- package/dist/worker/durable-object-websocket-handoff.d.ts.map +1 -1
- package/dist/worker/durable-object-websocket-handoff.js +22 -41
- package/dist/worker/durable-object-websocket-handoff.js.map +1 -1
- package/dist/worker/embed-generation.d.ts +9 -6
- package/dist/worker/embed-generation.d.ts.map +1 -1
- package/dist/worker/embed-generation.js +27 -6
- package/dist/worker/embed-generation.js.map +1 -1
- package/dist/worker/shims/fastify-websocket.d.ts +3 -0
- package/dist/worker/shims/fastify-websocket.d.ts.map +1 -0
- package/dist/worker/shims/fastify-websocket.js +3 -0
- package/dist/worker/shims/fastify-websocket.js.map +1 -0
- package/dist/worker/shims/fastify.d.ts +2 -10
- package/dist/worker/shims/fastify.d.ts.map +1 -1
- package/dist/worker/shims/fastify.js +14 -21
- package/dist/worker/shims/fastify.js.map +1 -1
- package/dist/worker/shims/postgres-browser.d.ts +1 -1
- package/dist/worker/shims/postgres-browser.d.ts.map +1 -1
- package/dist/worker/shims/postgres-browser.js +29 -20
- package/dist/worker/shims/postgres-browser.js.map +1 -1
- package/dist/worker/shims/sqlite-browser.d.ts +2 -3
- package/dist/worker/shims/sqlite-browser.d.ts.map +1 -1
- package/dist/worker/shims/sqlite-browser.js +2 -3
- package/dist/worker/shims/sqlite-browser.js.map +1 -1
- package/dist/worker/shims/sqlite.d.ts.map +1 -1
- package/dist/worker/shims/sqlite.js +56 -35
- package/dist/worker/shims/sqlite.js.map +1 -1
- package/dist/worker/shims/ws.d.ts.map +1 -1
- package/dist/worker/shims/ws.js +54 -39
- package/dist/worker/shims/ws.js.map +1 -1
- package/dist/worker/shims/zero-process-env.d.ts +1 -1
- package/dist/worker/shims/zero-process-env.d.ts.map +1 -1
- package/dist/worker/shims/zero-process-env.js +79 -8
- package/dist/worker/shims/zero-process-env.js.map +1 -1
- package/dist/worker/zero-cache-embed-cf.d.ts +5 -2
- package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
- package/dist/worker/zero-cache-embed-cf.js +339 -256
- package/dist/worker/zero-cache-embed-cf.js.map +1 -1
- package/dist/worker/zero-cache-run-worker.d.ts +2 -0
- package/dist/worker/zero-cache-run-worker.d.ts.map +1 -0
- package/dist/worker/zero-cache-run-worker.js +3 -0
- package/dist/worker/zero-cache-run-worker.js.map +1 -0
- package/package.json +6 -2
|
@@ -35,21 +35,25 @@
|
|
|
35
35
|
* again from durable DO SQLite. see plans/cf-do-idle-hibernation.md and
|
|
36
36
|
* `shouldHibernateIdleZeroCache` in ./zero-cache-do-idle.ts.
|
|
37
37
|
*/
|
|
38
|
-
import './shims/zero-process-env.js';
|
|
39
38
|
import { EventEmitter } from 'node:events';
|
|
40
|
-
// static import so wrangler can follow the dependency tree and bundle
|
|
41
|
-
// zero-cache with all its transitive deps + our shim aliases.
|
|
42
|
-
// @ts-expect-error — internal zero-cache module, no type declarations
|
|
43
|
-
import { runWorker as _runWorker } from '@rocicorp/zero/out/zero-cache/src/server/runner/run-worker.js';
|
|
44
|
-
import { setLogLevel } from '../log.js';
|
|
45
39
|
import { createBrowserProxy } from '../pg-proxy-browser.js';
|
|
46
|
-
import { DoBackend } from '../pg-proxy-do-backend.js';
|
|
47
|
-
import { resetReplicationState } from '../replication/handler.js';
|
|
40
|
+
import { DoBackend, releaseDoBackendInstanceCaches } from '../pg-proxy-do-backend.js';
|
|
41
|
+
import { deleteReplicationState, resetReplicationState, signalReplicationChange, } from '../replication/handler.js';
|
|
42
|
+
import { apiURLForCFInstance, dispatcherFastifyForCFInstance, logCFInstance, postgresURLForCFInstance, registerCFInstanceRuntime, releaseCFInstanceRuntime, setCFInstanceEnv, setCFInstanceProxy, sqliteDirectoryForCFInstance, sqlitePathForCFInstance, } from './cf-instance-runtime.js';
|
|
48
43
|
import { DurableObjectWebSocketHandoff, } from './durable-object-websocket-handoff.js';
|
|
49
|
-
import {
|
|
44
|
+
import { sweepCFInstanceSqliteHandles } from './embed-generation.js';
|
|
50
45
|
import { createLocalSqlBackend } from './local-sql-backend.js';
|
|
51
|
-
import {
|
|
46
|
+
import { acquireZeroProcessEnv } from './shims/zero-process-env.js';
|
|
47
|
+
// static import so wrangler follows zero-cache's dependency tree and shim aliases.
|
|
48
|
+
import { runWorker as _runWorker } from './zero-cache-run-worker.js';
|
|
52
49
|
const runWorkerFn = _runWorker;
|
|
50
|
+
const WORKER_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
51
|
+
function releaseGenerationWhenComplete(generation) {
|
|
52
|
+
if (generation.workerDone && generation.cleanupDone && !generation.cleanupFailed) {
|
|
53
|
+
deleteReplicationState(generation.runtime.instanceId);
|
|
54
|
+
releaseCFInstanceRuntime(generation.runtime);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
53
57
|
// tx-journal owner id for every pg session this embed opens. recovery at
|
|
54
58
|
// embed boot targets exactly this owner, so it can never roll back another
|
|
55
59
|
// client's live transaction on the shared upstream db (e.g. the app worker's
|
|
@@ -57,12 +61,12 @@ const runWorkerFn = _runWorker;
|
|
|
57
61
|
const EMBED_TX_OWNER = 'orez-embed';
|
|
58
62
|
// roll back journaled transactions a dead embed generation left on the
|
|
59
63
|
// remote SQL DO (upstream db sessions killed mid-transaction).
|
|
60
|
-
async function recoverRemoteTransactions(url, backendFetch) {
|
|
64
|
+
async function recoverRemoteTransactions(url, owner, backendFetch) {
|
|
61
65
|
const fetcher = backendFetch ?? fetch;
|
|
62
66
|
const resp = await fetcher(url, {
|
|
63
67
|
method: 'POST',
|
|
64
68
|
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify({ owner
|
|
69
|
+
body: JSON.stringify({ owner }),
|
|
66
70
|
});
|
|
67
71
|
if (!resp.ok) {
|
|
68
72
|
const text = await resp.text().catch(() => '');
|
|
@@ -79,27 +83,6 @@ function addProtocolSessionFactory(backend, createProtocolSession) {
|
|
|
79
83
|
* a DoBackend target for upstream/CVR/change Postgres connections.
|
|
80
84
|
*/
|
|
81
85
|
export async function startZeroCacheEmbedCF(opts) {
|
|
82
|
-
// wire orez's own logger from env, mirroring the node path's setLogLevel
|
|
83
|
-
// (index.ts). without this, OREZ_LOG_LEVEL=debug on a CF deploy is silently
|
|
84
|
-
// ignored and the replication poll loop is undebuggable in tails.
|
|
85
|
-
setLogLevel(opts.env?.OREZ_LOG_LEVEL || 'warn');
|
|
86
|
-
// generation hermeticity: the isolate (module state) outlives a stop() —
|
|
87
|
-
// CF reuses it across DO instance recreation after idle-hibernation
|
|
88
|
-
// teardown. a fresh embed must start from the replication-handler state a
|
|
89
|
-
// fresh isolate would have (lsn floor, stream watermark, schema caches);
|
|
90
|
-
// the reconnect reconciliation in handleStartReplication is designed to
|
|
91
|
-
// resume from durable state, not from a prior generation's module vars.
|
|
92
|
-
resetReplicationState();
|
|
93
|
-
// embed restart contract (see ./embed-generation.ts): reclaim what process
|
|
94
|
-
// death would have — sqlite handles the dead generation never closed
|
|
95
|
-
// (zero-cache relies on process-per-worker exit for these), and the
|
|
96
|
-
// fastify/ws shim instance registry (a dead change-streamer otherwise
|
|
97
|
-
// captures the new generation's replicator subscription and boot hangs).
|
|
98
|
-
const leakedHandles = sweepLeakedSqliteHandles();
|
|
99
|
-
if (leakedHandles > 0) {
|
|
100
|
-
console.warn(`[orez-zero-cache-cf] closed ${leakedHandles} sqlite handles leaked by the previous embed generation`);
|
|
101
|
-
}
|
|
102
|
-
resetFastifyRegistry();
|
|
103
86
|
const appId = opts.appId || 'zero';
|
|
104
87
|
const publications = opts.publications?.join(',') || `orez_${appId}_public`;
|
|
105
88
|
const readyTimeout = opts.readyTimeout ?? 30000;
|
|
@@ -107,210 +90,344 @@ export async function startZeroCacheEmbedCF(opts) {
|
|
|
107
90
|
const pgPassword = opts.pgPassword || '';
|
|
108
91
|
const backendUrl = opts.backendUrl || 'https://orez-do-backend.local';
|
|
109
92
|
const backendNamespace = opts.backendNamespace || appId;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// dead, so any journaled transaction it left mid-flight (DO eviction,
|
|
120
|
-
// deploy upgrade-kill) is rolled back BEFORE any pg session opens. without
|
|
121
|
-
// this, a partially-persisted tx (e.g. the change-streamer's cdc changeLog
|
|
122
|
-
// write) wedges replication on every subsequent boot.
|
|
123
|
-
localSql.recoverOrphanedTransactions();
|
|
124
|
-
await recoverRemoteTransactions(`${backendUrl.replace(/\/+$/, '')}/recover-txs?db=postgres&ns=${encodeURIComponent(backendNamespace)}`, opts.backendFetch);
|
|
125
|
-
const backends = {
|
|
126
|
-
postgres: createBackend('postgres'),
|
|
127
|
-
cvr: createBackend('zero_cvr'),
|
|
128
|
-
cdb: createBackend('zero_cdb'),
|
|
129
|
-
};
|
|
130
|
-
const proxyBackends = {
|
|
131
|
-
postgres: addProtocolSessionFactory(backends.postgres, () => createBackend('postgres')),
|
|
132
|
-
cvr: addProtocolSessionFactory(backends.cvr, () => createBackend('zero_cvr')),
|
|
133
|
-
cdb: addProtocolSessionFactory(backends.cdb, () => createBackend('zero_cdb')),
|
|
134
|
-
};
|
|
135
|
-
await Promise.all([
|
|
136
|
-
backends.postgres.waitReady,
|
|
137
|
-
backends.cvr.waitReady,
|
|
138
|
-
backends.cdb.waitReady,
|
|
139
|
-
]);
|
|
140
|
-
const proxy = await createBrowserProxy({
|
|
141
|
-
postgres: proxyBackends.postgres,
|
|
142
|
-
cvr: proxyBackends.cvr,
|
|
143
|
-
cdb: proxyBackends.cdb,
|
|
144
|
-
postgresReplicas: [],
|
|
145
|
-
}, {
|
|
146
|
-
pgUser,
|
|
93
|
+
if (!opts.apiFetch && (opts.env?.ZERO_MUTATE_URL || opts.env?.ZERO_QUERY_URL)) {
|
|
94
|
+
throw new Error('zero-cache CF embed: apiFetch is required with ZERO_MUTATE_URL or ZERO_QUERY_URL');
|
|
95
|
+
}
|
|
96
|
+
const runtime = registerCFInstanceRuntime({
|
|
97
|
+
apiFetch: opts.apiFetch,
|
|
98
|
+
doSqlite: opts.doSqlite,
|
|
99
|
+
env: opts.env ?? {},
|
|
100
|
+
instanceId: opts.instanceId,
|
|
101
|
+
log: opts.log,
|
|
147
102
|
pgPassword,
|
|
148
|
-
|
|
149
|
-
logLevel: opts.env?.ZERO_LOG_LEVEL || 'info',
|
|
103
|
+
pgUser,
|
|
150
104
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
105
|
+
const generation = {
|
|
106
|
+
cleanupDone: false,
|
|
107
|
+
cleanupFailed: false,
|
|
108
|
+
runtime,
|
|
109
|
+
workerDone: true,
|
|
154
110
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
111
|
+
const instanceId = runtime.instanceId;
|
|
112
|
+
const globalRecord = globalThis;
|
|
113
|
+
let processRecord;
|
|
114
|
+
let releaseProcessEnv = null;
|
|
115
|
+
const backendRoots = [];
|
|
116
|
+
let proxy = null;
|
|
117
|
+
let parent = null;
|
|
118
|
+
let parentEmitter = null;
|
|
119
|
+
let wrappedParent = null;
|
|
120
|
+
let isReady = false;
|
|
121
|
+
let stopping = false;
|
|
122
|
+
let runWorkerPromise = null;
|
|
123
|
+
let workerSettledPromise = null;
|
|
124
|
+
let workerError;
|
|
125
|
+
let workerFailed = false;
|
|
126
|
+
let startupFailure;
|
|
127
|
+
let shutdownPromise = null;
|
|
128
|
+
let fastifyInstance = null;
|
|
129
|
+
let readyTimer;
|
|
130
|
+
let debugEmbed = false;
|
|
131
|
+
const webSocketHandoff = new DurableObjectWebSocketHandoff(() => fastifyInstance);
|
|
132
|
+
const releaseProcessEnvWhenWorkerDone = () => {
|
|
133
|
+
if (!generation.workerDone || !releaseProcessEnv)
|
|
134
|
+
return;
|
|
135
|
+
releaseProcessEnv();
|
|
136
|
+
releaseProcessEnv = null;
|
|
171
137
|
};
|
|
172
|
-
|
|
173
|
-
|
|
138
|
+
const shutdown = () => {
|
|
139
|
+
if (shutdownPromise)
|
|
140
|
+
return shutdownPromise;
|
|
141
|
+
stopping = true;
|
|
142
|
+
isReady = false;
|
|
143
|
+
if (readyTimer)
|
|
144
|
+
clearTimeout(readyTimer);
|
|
145
|
+
shutdownPromise = (async () => {
|
|
146
|
+
const cleanupErrors = [];
|
|
147
|
+
let resourceCleanupFailed = false;
|
|
148
|
+
if (wrappedParent && runWorkerPromise && !generation.workerDone) {
|
|
149
|
+
try {
|
|
150
|
+
;
|
|
151
|
+
wrappedParent.kill('SIGTERM');
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
cleanupErrors.push(err);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (workerSettledPromise && !generation.workerDone) {
|
|
158
|
+
let timeout;
|
|
159
|
+
const workerStopped = await Promise.race([
|
|
160
|
+
workerSettledPromise.then(() => true),
|
|
161
|
+
new Promise((resolve) => {
|
|
162
|
+
timeout = setTimeout(() => resolve(false), WORKER_SHUTDOWN_TIMEOUT_MS);
|
|
163
|
+
}),
|
|
164
|
+
]);
|
|
165
|
+
if (timeout)
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
if (!workerStopped) {
|
|
168
|
+
resourceCleanupFailed = true;
|
|
169
|
+
cleanupErrors.push(new Error(`zero-cache CF embed: worker did not terminate within ${WORKER_SHUTDOWN_TIMEOUT_MS}ms`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (workerFailed && workerError !== startupFailure) {
|
|
173
|
+
cleanupErrors.push(workerError);
|
|
174
|
+
}
|
|
175
|
+
if (proxy) {
|
|
176
|
+
try {
|
|
177
|
+
await proxy.close();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
resourceCleanupFailed = true;
|
|
181
|
+
cleanupErrors.push(err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const backendResults = await Promise.allSettled(backendRoots.map((backend) => Promise.resolve().then(() => backend.close())));
|
|
185
|
+
for (const result of backendResults) {
|
|
186
|
+
if (result.status === 'rejected') {
|
|
187
|
+
resourceCleanupFailed = true;
|
|
188
|
+
cleanupErrors.push(result.reason);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
releaseDoBackendInstanceCaches(instanceId);
|
|
192
|
+
try {
|
|
193
|
+
webSocketHandoff.closeAll();
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
resourceCleanupFailed = true;
|
|
197
|
+
cleanupErrors.push(err);
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
sweepCFInstanceSqliteHandles(runtime);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
resourceCleanupFailed = true;
|
|
204
|
+
cleanupErrors.push(err);
|
|
205
|
+
}
|
|
206
|
+
releaseProcessEnvWhenWorkerDone();
|
|
207
|
+
parentEmitter?.removeAllListeners();
|
|
208
|
+
if (generation.workerDone)
|
|
209
|
+
parent?.removeAllListeners();
|
|
210
|
+
generation.cleanupDone = true;
|
|
211
|
+
generation.cleanupFailed = resourceCleanupFailed;
|
|
212
|
+
releaseGenerationWhenComplete(generation);
|
|
213
|
+
if (cleanupErrors.length > 0) {
|
|
214
|
+
throw new AggregateError(cleanupErrors, 'zero-cache CF embed: teardown failed');
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
return shutdownPromise;
|
|
174
218
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
219
|
+
const handleUnexpectedWorkerExit = () => {
|
|
220
|
+
if (!isReady || stopping)
|
|
221
|
+
return;
|
|
222
|
+
isReady = false;
|
|
223
|
+
if (!workerFailed) {
|
|
224
|
+
workerFailed = true;
|
|
225
|
+
workerError = new Error('zero-cache CF embed: runWorker exited after becoming ready');
|
|
226
|
+
}
|
|
227
|
+
void shutdown().catch((error) => {
|
|
228
|
+
logCFInstance(runtime, {
|
|
229
|
+
component: 'embed',
|
|
230
|
+
error,
|
|
231
|
+
event: 'unexpected-worker-exit-cleanup-failed',
|
|
232
|
+
});
|
|
233
|
+
console.error(`[orez-zero-cache-cf:${runtime.instanceId}] unexpected worker exit cleanup failed`, error);
|
|
234
|
+
});
|
|
183
235
|
};
|
|
184
|
-
|
|
185
|
-
;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
236
|
+
try {
|
|
237
|
+
releaseProcessEnv = acquireZeroProcessEnv();
|
|
238
|
+
processRecord = globalRecord.process;
|
|
239
|
+
const localSql = createLocalSqlBackend(opts.doSqlite);
|
|
240
|
+
const txOwner = `${EMBED_TX_OWNER}:${runtime.encodedId}`;
|
|
241
|
+
const instantiateBackend = (dbName) => new DoBackend(backendUrl, dbName, backendNamespace, {
|
|
242
|
+
allowTransactionalDDL: true,
|
|
243
|
+
fetch: dbName === 'postgres' ? opts.backendFetch : localSql.fetch,
|
|
244
|
+
instanceId,
|
|
245
|
+
signalReplication: () => signalReplicationChange(instanceId),
|
|
246
|
+
txOwner,
|
|
247
|
+
});
|
|
248
|
+
const createRootBackend = (dbName) => {
|
|
249
|
+
const backend = instantiateBackend(dbName);
|
|
250
|
+
backendRoots.push(backend);
|
|
251
|
+
return backend;
|
|
192
252
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
253
|
+
localSql.recoverOrphanedTransactions();
|
|
254
|
+
await recoverRemoteTransactions(`${backendUrl.replace(/\/+$/, '')}/recover-txs?db=postgres&ns=${encodeURIComponent(backendNamespace)}`, txOwner, opts.backendFetch);
|
|
255
|
+
const backends = {
|
|
256
|
+
postgres: createRootBackend('postgres'),
|
|
257
|
+
cvr: createRootBackend('zero_cvr'),
|
|
258
|
+
cdb: createRootBackend('zero_cdb'),
|
|
259
|
+
};
|
|
260
|
+
const proxyBackends = {
|
|
261
|
+
postgres: addProtocolSessionFactory(backends.postgres, () => instantiateBackend('postgres')),
|
|
262
|
+
cvr: addProtocolSessionFactory(backends.cvr, () => instantiateBackend('zero_cvr')),
|
|
263
|
+
cdb: addProtocolSessionFactory(backends.cdb, () => instantiateBackend('zero_cdb')),
|
|
264
|
+
};
|
|
265
|
+
const backendReadyResults = await Promise.allSettled([
|
|
266
|
+
backends.postgres.waitReady,
|
|
267
|
+
backends.cvr.waitReady,
|
|
268
|
+
backends.cdb.waitReady,
|
|
269
|
+
]);
|
|
270
|
+
const backendReadyErrors = backendReadyResults.flatMap((result) => result.status === 'rejected' ? [result.reason] : []);
|
|
271
|
+
if (backendReadyErrors.length === 1)
|
|
272
|
+
throw backendReadyErrors[0];
|
|
273
|
+
if (backendReadyErrors.length > 1) {
|
|
274
|
+
throw new AggregateError(backendReadyErrors, 'zero-cache CF embed: backend setup failed');
|
|
275
|
+
}
|
|
276
|
+
proxy = await createBrowserProxy({
|
|
277
|
+
postgres: proxyBackends.postgres,
|
|
278
|
+
cvr: proxyBackends.cvr,
|
|
279
|
+
cdb: proxyBackends.cdb,
|
|
280
|
+
postgresReplicas: [],
|
|
281
|
+
}, {
|
|
282
|
+
debugWire: opts.env?.OREZ_DEBUG_WIRE === '1',
|
|
283
|
+
instanceId,
|
|
284
|
+
pgUser,
|
|
285
|
+
pgPassword,
|
|
286
|
+
singleDb: false,
|
|
287
|
+
logLevel: opts.env?.ZERO_LOG_LEVEL || 'info',
|
|
288
|
+
});
|
|
289
|
+
setCFInstanceProxy(runtime, (port) => proxy?.handleConnection(port));
|
|
290
|
+
const createdParent = new EventEmitter();
|
|
291
|
+
parent = createdParent;
|
|
292
|
+
parentEmitter = new EventEmitter();
|
|
293
|
+
createdParent.send = (message, sendHandle) => {
|
|
294
|
+
parentEmitter?.emit('message', message, sendHandle);
|
|
295
|
+
return true;
|
|
296
|
+
};
|
|
297
|
+
createdParent.kill = (signal = 'SIGTERM') => {
|
|
298
|
+
createdParent.emit(signal, signal);
|
|
299
|
+
};
|
|
300
|
+
createdParent.pid = processRecord.pid ?? 1;
|
|
301
|
+
const env = {
|
|
302
|
+
SINGLE_PROCESS: '1',
|
|
303
|
+
NODE_ENV: 'development',
|
|
304
|
+
ZERO_APP_ID: appId,
|
|
305
|
+
ZERO_APP_PUBLICATIONS: publications,
|
|
306
|
+
ZERO_ADMIN_PASSWORD: opts.env?.ZERO_ADMIN_PASSWORD || crypto.randomUUID(),
|
|
307
|
+
ZERO_NUM_SYNC_WORKERS: opts.env?.ZERO_NUM_SYNC_WORKERS || '1',
|
|
308
|
+
ZERO_ENABLE_QUERY_PLANNER: 'false',
|
|
309
|
+
ZERO_UPSTREAM_MAX_CONNS: opts.env?.ZERO_UPSTREAM_MAX_CONNS || '2',
|
|
310
|
+
ZERO_CVR_MAX_CONNS: opts.env?.ZERO_CVR_MAX_CONNS || '2',
|
|
311
|
+
ZERO_CHANGE_MAX_CONNS: opts.env?.ZERO_CHANGE_MAX_CONNS || '2',
|
|
312
|
+
ZERO_LOG_LEVEL: opts.env?.ZERO_LOG_LEVEL || 'warn',
|
|
313
|
+
...opts.env,
|
|
314
|
+
ZERO_UPSTREAM_DB: postgresURLForCFInstance(instanceId, 'postgres', pgUser),
|
|
315
|
+
ZERO_CVR_DB: postgresURLForCFInstance(instanceId, 'zero_cvr', pgUser),
|
|
316
|
+
ZERO_CHANGE_DB: postgresURLForCFInstance(instanceId, 'zero_cdb', pgUser),
|
|
317
|
+
ZERO_REPLICA_FILE: sqlitePathForCFInstance(instanceId),
|
|
318
|
+
ZERO_STORAGE_DB_TMP_DIR: sqliteDirectoryForCFInstance(instanceId),
|
|
319
|
+
ZERO_PORT: String(runtime.basePort),
|
|
320
|
+
ZERO_TASK_ID: `orez-cf-${runtime.encodedId}`,
|
|
321
|
+
ZERO_SHADOW_SYNC_ENABLED: 'false',
|
|
322
|
+
};
|
|
323
|
+
if (opts.apiFetch) {
|
|
324
|
+
for (const key of ['ZERO_MUTATE_URL', 'ZERO_QUERY_URL']) {
|
|
325
|
+
if (env[key])
|
|
326
|
+
env[key] = apiURLForCFInstance(instanceId, env[key]);
|
|
249
327
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
328
|
+
}
|
|
329
|
+
setCFInstanceEnv(runtime, env);
|
|
330
|
+
resetReplicationState(instanceId, env, opts.log);
|
|
331
|
+
debugEmbed = env.OREZ_DEBUG_EMBED === '1';
|
|
332
|
+
wrappedParent = new Proxy(createdParent, {
|
|
333
|
+
get(target, prop, receiver) {
|
|
334
|
+
if (prop === 'onMessageType') {
|
|
335
|
+
return (type, handler) => {
|
|
336
|
+
target.on('message', (data, sendHandle) => {
|
|
337
|
+
if (Array.isArray(data) && data.length === 2 && data[0] === type) {
|
|
338
|
+
handler(data[1], sendHandle);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
return receiver;
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (prop === 'onceMessageType') {
|
|
345
|
+
return (type, handler) => {
|
|
346
|
+
const listener = (data, sendHandle) => {
|
|
347
|
+
if (Array.isArray(data) && data.length === 2 && data[0] === type) {
|
|
348
|
+
target.off('message', listener);
|
|
349
|
+
handler(data[1], sendHandle);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
target.on('message', listener);
|
|
353
|
+
return receiver;
|
|
257
354
|
};
|
|
258
|
-
|
|
259
|
-
|
|
355
|
+
}
|
|
356
|
+
const value = Reflect.get(target, prop, target);
|
|
357
|
+
if (typeof value !== 'function')
|
|
358
|
+
return value;
|
|
359
|
+
return (...args) => {
|
|
360
|
+
const result = value.apply(target, args);
|
|
361
|
+
return result === target ? receiver : result;
|
|
260
362
|
};
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
366
|
+
readyTimer = setTimeout(() => {
|
|
367
|
+
reject(new Error(`zero-cache CF embed: timed out waiting for ready after ${readyTimeout}ms`));
|
|
368
|
+
}, readyTimeout);
|
|
369
|
+
parentEmitter?.on('message', (msg) => {
|
|
370
|
+
if (debugEmbed) {
|
|
371
|
+
logCFInstance(runtime, {
|
|
372
|
+
component: 'embed',
|
|
373
|
+
event: 'parent-message',
|
|
374
|
+
message: msg,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (!stopping && Array.isArray(msg) && msg[0] === 'ready') {
|
|
378
|
+
if (readyTimer)
|
|
379
|
+
clearTimeout(readyTimer);
|
|
380
|
+
isReady = true;
|
|
381
|
+
resolve();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
generation.workerDone = false;
|
|
386
|
+
runWorkerPromise = Promise.resolve().then(() => runWorkerFn(wrappedParent, env));
|
|
387
|
+
void runWorkerPromise.catch((err) => {
|
|
388
|
+
if (debugEmbed) {
|
|
389
|
+
logCFInstance(runtime, {
|
|
390
|
+
component: 'embed',
|
|
391
|
+
error: err,
|
|
392
|
+
event: 'worker-error',
|
|
393
|
+
});
|
|
261
394
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
clearTimeout(readyTimer);
|
|
284
|
-
isReady = true;
|
|
285
|
-
resolve();
|
|
395
|
+
});
|
|
396
|
+
workerSettledPromise = runWorkerPromise.then(() => {
|
|
397
|
+
generation.workerDone = true;
|
|
398
|
+
releaseProcessEnvWhenWorkerDone();
|
|
399
|
+
handleUnexpectedWorkerExit();
|
|
400
|
+
if (generation.cleanupDone)
|
|
401
|
+
parent?.removeAllListeners();
|
|
402
|
+
releaseGenerationWhenComplete(generation);
|
|
403
|
+
}, (error) => {
|
|
404
|
+
workerError = error;
|
|
405
|
+
workerFailed = true;
|
|
406
|
+
generation.workerDone = true;
|
|
407
|
+
releaseProcessEnvWhenWorkerDone();
|
|
408
|
+
handleUnexpectedWorkerExit();
|
|
409
|
+
if (generation.cleanupDone)
|
|
410
|
+
parent?.removeAllListeners();
|
|
411
|
+
releaseGenerationWhenComplete(generation);
|
|
412
|
+
});
|
|
413
|
+
const workerStartupPromise = runWorkerPromise.then(() => {
|
|
414
|
+
if (!isReady) {
|
|
415
|
+
throw new Error('zero-cache CF embed: runWorker exited before ready');
|
|
286
416
|
}
|
|
287
417
|
});
|
|
288
|
-
});
|
|
289
|
-
// start zero-cache
|
|
290
|
-
runWorkerPromise = runWorkerFn(wrappedParent, env).catch((err) => {
|
|
291
|
-
if (debugEmbed)
|
|
292
|
-
console.error('[orez-zero-cache-cf] runWorker error', err);
|
|
293
|
-
if (!isReady) {
|
|
294
|
-
throw err;
|
|
295
|
-
}
|
|
296
|
-
// after ready, errors during shutdown are expected
|
|
297
|
-
});
|
|
298
|
-
const workerStartupPromise = runWorkerPromise.then(() => {
|
|
299
|
-
if (!isReady) {
|
|
300
|
-
throw new Error('zero-cache CF embed: runWorker exited before ready');
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
// wait for ready
|
|
304
|
-
try {
|
|
305
418
|
await Promise.race([readyPromise, workerStartupPromise]);
|
|
419
|
+
fastifyInstance = dispatcherFastifyForCFInstance(instanceId);
|
|
306
420
|
}
|
|
307
|
-
catch (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
421
|
+
catch (startupError) {
|
|
422
|
+
startupFailure = startupError;
|
|
423
|
+
try {
|
|
424
|
+
await shutdown();
|
|
425
|
+
}
|
|
426
|
+
catch (cleanupError) {
|
|
427
|
+
throw new AggregateError([startupError, cleanupError], 'zero-cache CF embed: startup failed and teardown also failed', { cause: startupError });
|
|
428
|
+
}
|
|
429
|
+
throw startupError;
|
|
311
430
|
}
|
|
312
|
-
// get the fastify instance (set by our shim during init)
|
|
313
|
-
fastifyInstance = globalThis.__orez_fastify_instance;
|
|
314
431
|
return {
|
|
315
432
|
get ready() {
|
|
316
433
|
return isReady;
|
|
@@ -330,41 +447,7 @@ export async function startZeroCacheEmbedCF(opts) {
|
|
|
330
447
|
}
|
|
331
448
|
return handleHttpRequest(request, url, fastifyInstance);
|
|
332
449
|
},
|
|
333
|
-
|
|
334
|
-
isReady = false;
|
|
335
|
-
wrappedParent.kill('SIGTERM');
|
|
336
|
-
if (runWorkerPromise) {
|
|
337
|
-
await Promise.race([runWorkerPromise, new Promise((r) => setTimeout(r, 5000))]);
|
|
338
|
-
}
|
|
339
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
340
|
-
proxy.close();
|
|
341
|
-
await Promise.all([
|
|
342
|
-
backends.postgres.close(),
|
|
343
|
-
backends.cvr.close(),
|
|
344
|
-
backends.cdb.close(),
|
|
345
|
-
]);
|
|
346
|
-
// restore all modified globals
|
|
347
|
-
if (origExit) {
|
|
348
|
-
;
|
|
349
|
-
globalThis.process.exit = origExit;
|
|
350
|
-
}
|
|
351
|
-
if (origNodeEnv !== undefined) {
|
|
352
|
-
;
|
|
353
|
-
globalThis.process.env.NODE_ENV = origNodeEnv;
|
|
354
|
-
}
|
|
355
|
-
if (origKill) {
|
|
356
|
-
;
|
|
357
|
-
globalThis.process.kill = origKill;
|
|
358
|
-
}
|
|
359
|
-
if (opts.apiFetch) {
|
|
360
|
-
;
|
|
361
|
-
globalThis.fetch = origFetch;
|
|
362
|
-
}
|
|
363
|
-
delete globalThis.process.env.SINGLE_PROCESS;
|
|
364
|
-
delete globalThis.__orez_proxy_connect;
|
|
365
|
-
delete globalThis.__orez_proxy_user;
|
|
366
|
-
delete globalThis.__orez_proxy_password;
|
|
367
|
-
},
|
|
450
|
+
stop: shutdown,
|
|
368
451
|
};
|
|
369
452
|
}
|
|
370
453
|
// -- HTTP request handling --
|