@lunora/runtime 1.0.0-alpha.10 → 1.0.0-alpha.11
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/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
|
|
2
|
-
export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-
|
|
2
|
+
export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-BNYeYQqL.mjs';
|
|
3
3
|
export { createCrossShardRelationCapabilities } from './packem_shared/createCrossShardRelationCapabilities-C0KOf7er.mjs';
|
|
4
4
|
export { DEFAULT_REGISTRY_CACHE_TTL_MS, SHARD_REGISTRY_DO_NAME, createDynamicShardRegistry } from './packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-ocax8v0n.mjs';
|
|
5
5
|
export { LunoraError, toErrorResponse } from './packem_shared/LunoraError-CL0aOtpo.mjs';
|
|
@@ -282,6 +282,45 @@ const buildAuthAdminRoutes = (deps) => {
|
|
|
282
282
|
return routes;
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
+
const MAX_BATCH_ENTRIES = 500;
|
|
286
|
+
|
|
287
|
+
const normalizeBatchCall = (raw, index, defaultShard) => {
|
|
288
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
289
|
+
throw new LunoraError("each batch call must be an object", { code: "BAD_REQUEST", status: 400 });
|
|
290
|
+
}
|
|
291
|
+
const call = raw;
|
|
292
|
+
if (typeof call.functionPath !== "string") {
|
|
293
|
+
throw new LunoraError("each batch call needs a string `functionPath`", { code: "BAD_REQUEST", status: 400 });
|
|
294
|
+
}
|
|
295
|
+
if (call.functionPath.startsWith("__lunora_relation__:") || call.functionPath.startsWith("__lunora_admin__")) {
|
|
296
|
+
throw new LunoraError("reserved function path cannot be batched", { code: "FORBIDDEN", status: 403 });
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
entry: {
|
|
300
|
+
args: call.args === void 0 ? {} : call.args,
|
|
301
|
+
clientId: typeof call.clientId === "string" ? call.clientId : void 0,
|
|
302
|
+
clientSeq: typeof call.clientSeq === "number" ? call.clientSeq : void 0,
|
|
303
|
+
functionPath: call.functionPath,
|
|
304
|
+
id: typeof call.id === "number" ? call.id : index,
|
|
305
|
+
mutationId: typeof call.mutationId === "string" ? call.mutationId : void 0
|
|
306
|
+
},
|
|
307
|
+
shardKey: typeof call.shardKey === "string" ? call.shardKey : defaultShard
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
const groupBatchCallsByShard = (calls, defaultShard) => {
|
|
311
|
+
if (calls.length > MAX_BATCH_ENTRIES) {
|
|
312
|
+
throw new LunoraError(`RPC batch exceeds the ${String(MAX_BATCH_ENTRIES)}-call limit`, { code: "BAD_REQUEST", status: 400 });
|
|
313
|
+
}
|
|
314
|
+
const groups = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const [index, raw] of calls.entries()) {
|
|
316
|
+
const { entry, shardKey } = normalizeBatchCall(raw, index, defaultShard);
|
|
317
|
+
const group = groups.get(shardKey) ?? [];
|
|
318
|
+
group.push(entry);
|
|
319
|
+
groups.set(shardKey, group);
|
|
320
|
+
}
|
|
321
|
+
return groups;
|
|
322
|
+
};
|
|
323
|
+
|
|
285
324
|
const MAX_BODY_BYTES = 1048576;
|
|
286
325
|
const readBodyTextWithLimit = async (request, limit = MAX_BODY_BYTES) => {
|
|
287
326
|
if (!request.body) {
|
|
@@ -1558,6 +1597,7 @@ const buildWorkflowsAdminRoutes = (deps) => {
|
|
|
1558
1597
|
|
|
1559
1598
|
const NDJSON_ENCODER = new TextEncoder();
|
|
1560
1599
|
const RPC_PATH = "/_lunora/rpc";
|
|
1600
|
+
const RPC_BATCH_PATH = "/_lunora/rpc-batch";
|
|
1561
1601
|
const WS_PATH = "/_lunora/ws";
|
|
1562
1602
|
const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
|
|
1563
1603
|
const CRON_JOBS_RUN_PATH = "/_lunora/admin/cron-jobs/run";
|
|
@@ -2298,6 +2338,119 @@ const createWorker = (options) => {
|
|
|
2298
2338
|
return dispatchSingleShard(envelope.functionPath, envelope.args ?? {}, shardKey, forwardedHeaders, sinkContext);
|
|
2299
2339
|
}
|
|
2300
2340
|
};
|
|
2341
|
+
const handleBatchRpc = async (request, env, context) => {
|
|
2342
|
+
if (request.method !== "POST") {
|
|
2343
|
+
throw new LunoraError("RPC batch endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
2344
|
+
}
|
|
2345
|
+
const text = await readBodyTextWithLimit(request);
|
|
2346
|
+
let body;
|
|
2347
|
+
try {
|
|
2348
|
+
body = JSON.parse(text);
|
|
2349
|
+
} catch {
|
|
2350
|
+
throw new LunoraError("RPC batch body must be valid JSON", { code: "BAD_REQUEST", status: 400 });
|
|
2351
|
+
}
|
|
2352
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
2353
|
+
throw new LunoraError("RPC batch body must be an object", { code: "BAD_REQUEST", status: 400 });
|
|
2354
|
+
}
|
|
2355
|
+
const { calls } = body;
|
|
2356
|
+
if (!Array.isArray(calls)) {
|
|
2357
|
+
throw new LunoraError("RPC batch `calls` must be an array", { code: "BAD_REQUEST", status: 400 });
|
|
2358
|
+
}
|
|
2359
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
|
|
2360
|
+
const groups = groupBatchCallsByShard(calls, defaultShard);
|
|
2361
|
+
await Promise.all(
|
|
2362
|
+
[...groups.entries()].flatMap(
|
|
2363
|
+
([shardKey, entries]) => entries.map((entry) => authorizeRpcEnvelope({ functionPath: entry.functionPath, shardKey }, identity))
|
|
2364
|
+
)
|
|
2365
|
+
);
|
|
2366
|
+
const { observability } = options;
|
|
2367
|
+
const sinkContext = context ? {
|
|
2368
|
+
waitUntil: (promise) => {
|
|
2369
|
+
context.waitUntil?.(promise);
|
|
2370
|
+
}
|
|
2371
|
+
} : void 0;
|
|
2372
|
+
const results = [];
|
|
2373
|
+
let latestBookmark;
|
|
2374
|
+
const slotError = (entry, status, code, message) => {
|
|
2375
|
+
return { body: { error: { code, message } }, id: entry.id, status };
|
|
2376
|
+
};
|
|
2377
|
+
const failSubBatch = (entries, status, code, message, eventFor) => {
|
|
2378
|
+
for (const entry of entries) {
|
|
2379
|
+
emitRpcEvent(observability, eventFor(entry), sinkContext);
|
|
2380
|
+
results.push(slotError(entry, status, code, message));
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
const emitEntryEvents = (entries, shardKey, durationMs, statusById, fallbackStatus) => {
|
|
2384
|
+
for (const entry of entries) {
|
|
2385
|
+
const status = statusById.get(entry.id) ?? fallbackStatus;
|
|
2386
|
+
const ok = status < 400;
|
|
2387
|
+
emitRpcEvent(
|
|
2388
|
+
observability,
|
|
2389
|
+
{
|
|
2390
|
+
durationMs,
|
|
2391
|
+
functionPath: entry.functionPath,
|
|
2392
|
+
ok,
|
|
2393
|
+
shardKey,
|
|
2394
|
+
...ok ? {} : { error: { code: "SHARD_ERROR", message: `batched call returned ${String(status)}`, status } }
|
|
2395
|
+
},
|
|
2396
|
+
sinkContext
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
await Promise.all(
|
|
2401
|
+
[...groups.entries()].map(async ([shardKey, entries]) => {
|
|
2402
|
+
const headers = new Headers(forwardedHeaders);
|
|
2403
|
+
headers.set("content-type", "application/json");
|
|
2404
|
+
const subRequest = new Request("https://shard.internal/rpc-batch", { body: JSON.stringify({ calls: entries }), headers, method: "POST" });
|
|
2405
|
+
const subStartedAt = Date.now();
|
|
2406
|
+
let response;
|
|
2407
|
+
try {
|
|
2408
|
+
response = await forwardToShard(shardDO, shardKey, subRequest);
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
const durationMs2 = Date.now() - subStartedAt;
|
|
2411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2412
|
+
failSubBatch(entries, 502, "SHARD_UNAVAILABLE", message, (entry) => buildErrorEvent(entry.functionPath, durationMs2, error, { shardKey }));
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const durationMs = Date.now() - subStartedAt;
|
|
2416
|
+
const bookmark = response.headers.get("x-d1-bookmark");
|
|
2417
|
+
if (bookmark) {
|
|
2418
|
+
latestBookmark = bookmark;
|
|
2419
|
+
}
|
|
2420
|
+
let parsed;
|
|
2421
|
+
try {
|
|
2422
|
+
parsed = await response.json();
|
|
2423
|
+
} catch {
|
|
2424
|
+
const message = `shard batch returned a non-JSON response (${String(response.status)})`;
|
|
2425
|
+
failSubBatch(entries, response.status, "SHARD_ERROR", message, (entry) => {
|
|
2426
|
+
return {
|
|
2427
|
+
durationMs,
|
|
2428
|
+
error: { code: "SHARD_ERROR", message, status: response.status },
|
|
2429
|
+
functionPath: entry.functionPath,
|
|
2430
|
+
ok: false,
|
|
2431
|
+
shardKey
|
|
2432
|
+
};
|
|
2433
|
+
});
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const entryResults = Array.isArray(parsed.results) ? parsed.results : [];
|
|
2437
|
+
const statusById = new Map(entryResults.map((entry) => [entry.id, entry.status ?? response.status]));
|
|
2438
|
+
const seenIds = new Set(entryResults.map((entry) => entry.id));
|
|
2439
|
+
emitEntryEvents(entries, shardKey, durationMs, statusById, response.status);
|
|
2440
|
+
results.push(...entryResults);
|
|
2441
|
+
for (const entry of entries) {
|
|
2442
|
+
if (!seenIds.has(entry.id)) {
|
|
2443
|
+
results.push(slotError(entry, response.status, "SHARD_ERROR", `shard batch omitted result for call ${String(entry.id)}`));
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
})
|
|
2447
|
+
);
|
|
2448
|
+
const responseHeaders = { "content-type": "application/json" };
|
|
2449
|
+
if (latestBookmark !== void 0) {
|
|
2450
|
+
responseHeaders["x-d1-bookmark"] = latestBookmark;
|
|
2451
|
+
}
|
|
2452
|
+
return Response.json({ results }, { headers: responseHeaders, status: 200 });
|
|
2453
|
+
};
|
|
2301
2454
|
const serverQuery = async (request, env, reference, args = {}, callOptions = {}) => {
|
|
2302
2455
|
try {
|
|
2303
2456
|
const functionPath = reference.__lunoraRef;
|
|
@@ -2461,6 +2614,7 @@ const createWorker = (options) => {
|
|
|
2461
2614
|
const internalRoutes = {
|
|
2462
2615
|
[WS_PATH]: (request, env, url) => handleWebSocketUpgrade(request, env, url),
|
|
2463
2616
|
[RPC_PATH]: (request, env, _url, context) => handleRpc(request, env, context),
|
|
2617
|
+
[RPC_BATCH_PATH]: (request, env, _url, context) => handleBatchRpc(request, env, context),
|
|
2464
2618
|
[SCHEDULER_DISPATCH_PATH]: (request, env) => handleSchedulerDispatch(request, env),
|
|
2465
2619
|
[CRON_JOBS_RUN_PATH]: (request, env) => handleRunCronJob(request, env),
|
|
2466
2620
|
// Extracted handler clusters built above, merged in (mirroring the auth
|