@lunora/runtime 1.0.0-alpha.10 → 1.0.0-alpha.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/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
2
- export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-BEv9IBWS.mjs';
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';
6
6
  export { emitLogEvent, emitRpcEvent } from './packem_shared/emitLogEvent-pEdtqAK8.mjs';
7
7
  export { analyticsEngineSink, combineSinks, consoleSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-DqEvrQs0.mjs';
8
- export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggregate } from './packem_shared/createQueryCoordinator-Cbds9cUI.mjs';
8
+ export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggregate } from './packem_shared/createQueryCoordinator-ZeZYUPNu.mjs';
9
9
  export { applyJurisdiction, resolveShard } from './packem_shared/applyJurisdiction-BkZtTkct.mjs';
10
10
  export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-DbISh_Wi.mjs';
11
11
  export { NOOP_EXECUTION_CONTEXT } from './packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.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
@@ -547,7 +547,16 @@ const mergeTopK = (values, strategy) => {
547
547
  }
548
548
  }
549
549
  const direction = strategy.direction ?? "desc";
550
- collected.sort((a, b) => direction === "asc" ? a.score - b.score : b.score - a.score);
550
+ const ascending = (x, y) => {
551
+ if (x < y) {
552
+ return -1;
553
+ }
554
+ if (x > y) {
555
+ return 1;
556
+ }
557
+ return 0;
558
+ };
559
+ collected.sort((a, b) => direction === "asc" ? ascending(a.score, b.score) : ascending(b.score, a.score));
551
560
  return collected.slice(0, strategy.k).map((entry) => entry.row);
552
561
  };
553
562
  const mergeShardResults = (values, strategy) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",