@supacloud/elysia 0.22.0 → 0.23.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/index.d.ts +13 -2
- package/dist/index.js +92 -49
- package/dist/worker.d.ts +7 -0
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Elysia, type AnyElysia, type Context, type StatusMap } from "elysia";
|
|
2
2
|
import type { CommandRuntimeAudit, CommandRuntimeAuthorizer, CommandRuntimeGovernance, CommandRuntimeInvocation, CommandRuntimeMiddleware } from "@supacloud/app";
|
|
3
|
+
import { PendingWorkRegistry } from "@supacloud/app/runtime";
|
|
3
4
|
import { type CommandPreview } from "@supacloud/contracts";
|
|
4
5
|
import { type HttpPolicyRegistry } from "./http-policy";
|
|
5
6
|
export { HttpPolicyConfigurationError } from "./http-policy";
|
|
@@ -247,7 +248,7 @@ export type ApplicationHttpContext<Http extends AnyElysia = Elysia> = Omit<Conte
|
|
|
247
248
|
store: Http["~Singleton"]["store"];
|
|
248
249
|
derive: Http["~Singleton"]["derive"] & Http["~Ephemeral"]["derive"];
|
|
249
250
|
}>, "body" | "params" | "query" | "headers" | "cookie"> & Pick<HttpContext, "body" | "params" | "query" | "headers" | "cookie">;
|
|
250
|
-
export type HttpRequestContextFactory<Http extends AnyElysia = Elysia, Value = unknown> = (request: Request, context: ApplicationHttpContext<Http
|
|
251
|
+
export type HttpRequestContextFactory<Http extends AnyElysia = Elysia, Value = unknown> = (request: Request, context: ApplicationHttpContext<Http>, signal?: AbortSignal) => Value | Promise<Value>;
|
|
251
252
|
export interface ApplicationOptions<Http extends AnyElysia = Elysia, RequestContext = unknown> {
|
|
252
253
|
name?: string;
|
|
253
254
|
/** false rejects extra schema properties instead of silently removing them. */
|
|
@@ -272,6 +273,11 @@ export interface ApplicationOptions<Http extends AnyElysia = Elysia, RequestCont
|
|
|
272
273
|
onExecution?: ExecutionObserver;
|
|
273
274
|
/** Optional read-only OpenAPI and GraphQL documentation endpoints. */
|
|
274
275
|
documentation?: ApplicationDocumentationOptions;
|
|
276
|
+
/** Bounded lifecycle work accounting; registries remain owned by each host. */
|
|
277
|
+
pendingWork?: {
|
|
278
|
+
capacity?: number;
|
|
279
|
+
shutdownTimeoutMs?: number;
|
|
280
|
+
};
|
|
275
281
|
}
|
|
276
282
|
/** Lifecycle controls exposed by createApplication for explicit host startup/shutdown. */
|
|
277
283
|
export interface ApplicationLifecycle {
|
|
@@ -279,6 +285,11 @@ export interface ApplicationLifecycle {
|
|
|
279
285
|
readonly destroyed: boolean;
|
|
280
286
|
initialize(): Promise<void>;
|
|
281
287
|
destroy(): Promise<void>;
|
|
288
|
+
readonly pendingWork: PendingWorkRegistry;
|
|
289
|
+
waitForIdle(options?: {
|
|
290
|
+
timeoutMs?: number;
|
|
291
|
+
includeBackground?: boolean;
|
|
292
|
+
}): Promise<void>;
|
|
282
293
|
}
|
|
283
294
|
export interface JobInvocation {
|
|
284
295
|
job: CompiledJob;
|
|
@@ -349,7 +360,7 @@ interface HttpContext {
|
|
|
349
360
|
* and passed to the controller input as `scope`. Request-scoped controllers
|
|
350
361
|
* are looked up on that scope, everything else on `services`.
|
|
351
362
|
*/
|
|
352
|
-
export declare function createModulePlugin<const Services extends Record<string, unknown>, const Http extends AnyElysia = Elysia>(compiled: CompiledModule, services: Services, ctxFactory?: HttpRequestContextFactory<Http>, options?: Pick<ApplicationOptions<Http>, "http" | "httpPolicies" | "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize">, imported?: Record<string, Record<string, unknown
|
|
363
|
+
export declare function createModulePlugin<const Services extends Record<string, unknown>, const Http extends AnyElysia = Elysia>(compiled: CompiledModule, services: Services, ctxFactory?: HttpRequestContextFactory<Http>, options?: Pick<ApplicationOptions<Http>, "http" | "httpPolicies" | "commandGovernance" | "commandExecutor" | "errorMapper" | "onExecution" | "normalize">, imported?: Record<string, Record<string, unknown>>, pendingWork?: PendingWorkRegistry): import("elysia/types").LocalHookReturn<"", "local", {
|
|
353
364
|
decorator: {} & Http["~Singleton"]["decorator"];
|
|
354
365
|
store: ({} & Http["~Singleton"]["store"] extends infer T_1 ? { [K_1 in keyof T_1]: T_1[K_1]; } : never) extends infer T ? { [K in keyof T]: T[K]; } : never;
|
|
355
366
|
derive: {} & Http["~Singleton"]["derive"];
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Elysia as Elysia3, setupTypebox, Validator as Validator2 } from "elysia";
|
|
3
|
+
import { PendingWorkRegistry as PendingWorkRegistry2, PendingWorkTimeoutError } from "@supacloud/app/runtime";
|
|
3
4
|
import { decodeCommandPreview } from "@supacloud/contracts";
|
|
4
5
|
|
|
5
6
|
// src/command-errors.ts
|
|
@@ -1426,6 +1427,7 @@ function cloneValue(value) {
|
|
|
1426
1427
|
return structuredClone(value);
|
|
1427
1428
|
}
|
|
1428
1429
|
// src/worker.ts
|
|
1430
|
+
import { PendingWorkRegistry } from "@supacloud/app/runtime";
|
|
1429
1431
|
function workerFailureMessage(error) {
|
|
1430
1432
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Worker job failed";
|
|
1431
1433
|
const normalized = message.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
@@ -1558,6 +1560,11 @@ class SupaCloudWorker {
|
|
|
1558
1560
|
workerId;
|
|
1559
1561
|
concurrency;
|
|
1560
1562
|
pollIntervalMs;
|
|
1563
|
+
shutdownTimeoutMs;
|
|
1564
|
+
work = new PendingWorkRegistry;
|
|
1565
|
+
get pendingWork() {
|
|
1566
|
+
return this.work;
|
|
1567
|
+
}
|
|
1561
1568
|
executor;
|
|
1562
1569
|
observer;
|
|
1563
1570
|
onError;
|
|
@@ -1588,6 +1595,7 @@ class SupaCloudWorker {
|
|
|
1588
1595
|
}));
|
|
1589
1596
|
this.concurrency = this.capturePositiveInteger(options.concurrency, 1, 1000, "concurrency");
|
|
1590
1597
|
this.pollIntervalMs = this.captureNonNegativeInteger(options.pollIntervalMs, 1000, 300000, "pollIntervalMs");
|
|
1598
|
+
this.shutdownTimeoutMs = this.captureNonNegativeInteger(options.shutdownTimeoutMs, 5000, 2147483647, "shutdownTimeoutMs");
|
|
1591
1599
|
this.executor = options.executor;
|
|
1592
1600
|
this.observer = options.onExecution;
|
|
1593
1601
|
this.onError = options.onError;
|
|
@@ -1640,11 +1648,13 @@ class SupaCloudWorker {
|
|
|
1640
1648
|
return;
|
|
1641
1649
|
if (this.stateValue === "starting" && this.startPromise)
|
|
1642
1650
|
return this.startPromise;
|
|
1643
|
-
if (this.stateValue === "stopping"
|
|
1644
|
-
await this.
|
|
1651
|
+
if (this.stateValue === "stopping")
|
|
1652
|
+
await this.stop();
|
|
1645
1653
|
this.stopRequested = false;
|
|
1654
|
+
if (this.work.closed)
|
|
1655
|
+
this.work = new PendingWorkRegistry;
|
|
1646
1656
|
this.stateValue = "starting";
|
|
1647
|
-
const startPromise = Promise.resolve().then(() => this.initialize()).then(async () => {
|
|
1657
|
+
const startPromise = this.work.run({ name: "worker.startup", kind: "startup" }, () => Promise.resolve().then(() => this.initialize())).then(async () => {
|
|
1648
1658
|
if (this.stopRequested || this.stateValue !== "starting") {
|
|
1649
1659
|
await this.releaseServices();
|
|
1650
1660
|
return;
|
|
@@ -1667,9 +1677,12 @@ class SupaCloudWorker {
|
|
|
1667
1677
|
return this.stopPromise;
|
|
1668
1678
|
this.stopRequested = true;
|
|
1669
1679
|
this.stateValue = "stopping";
|
|
1670
|
-
this.
|
|
1680
|
+
this.work.close();
|
|
1671
1681
|
const startPromise = this.startPromise;
|
|
1672
|
-
this.stopPromise = (async () => {
|
|
1682
|
+
this.stopPromise = Promise.resolve().then(async () => {
|
|
1683
|
+
this.claimController?.abort(new DOMException("Worker stopped", "AbortError"));
|
|
1684
|
+
this.work.dispose();
|
|
1685
|
+
await this.work.waitForIdle({ timeoutMs: this.shutdownTimeoutMs });
|
|
1673
1686
|
await startPromise?.catch(() => {
|
|
1674
1687
|
return;
|
|
1675
1688
|
});
|
|
@@ -1681,7 +1694,10 @@ class SupaCloudWorker {
|
|
|
1681
1694
|
this.loopPromise = undefined;
|
|
1682
1695
|
this.startPromise = undefined;
|
|
1683
1696
|
this.stopPromise = undefined;
|
|
1684
|
-
})()
|
|
1697
|
+
}).catch((error) => {
|
|
1698
|
+
this.stopPromise = undefined;
|
|
1699
|
+
throw error;
|
|
1700
|
+
});
|
|
1685
1701
|
return this.stopPromise;
|
|
1686
1702
|
}
|
|
1687
1703
|
async runOnce() {
|
|
@@ -1692,20 +1708,22 @@ class SupaCloudWorker {
|
|
|
1692
1708
|
if (!controller)
|
|
1693
1709
|
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker claim lifecycle is unavailable");
|
|
1694
1710
|
controller.signal.throwIfAborted();
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
return null;
|
|
1698
|
-
|
|
1711
|
+
return this.work.run({ name: "worker.claim", kind: "job" }, async (signal) => {
|
|
1712
|
+
const claim = await this.transport.claim(controller.signal);
|
|
1713
|
+
return claim === null ? null : this.executeClaim(claim, signal);
|
|
1714
|
+
});
|
|
1699
1715
|
}
|
|
1700
1716
|
async processClaim(rawClaim) {
|
|
1717
|
+
return this.work.run({ name: "worker.job", kind: "job" }, (signal) => this.executeClaim(rawClaim, signal));
|
|
1718
|
+
}
|
|
1719
|
+
async executeClaim(rawClaim, signal) {
|
|
1701
1720
|
const claim = safeClaim(this.mapClaim(rawClaim));
|
|
1702
1721
|
const entry = this.registry.get(claim.jobName);
|
|
1703
|
-
const controller = new AbortController;
|
|
1704
1722
|
const context = Object.freeze({
|
|
1705
1723
|
jobId: claim.id,
|
|
1706
1724
|
jobName: claim.jobName,
|
|
1707
1725
|
attempt: claim.attempt ?? 1,
|
|
1708
|
-
signal:
|
|
1726
|
+
signal: new AbortController().signal
|
|
1709
1727
|
});
|
|
1710
1728
|
if (!entry) {
|
|
1711
1729
|
const receipt = await this.failClaim(rawClaim, claim, new WorkerRegistrationError("WORKER_INVALID_JOB", `Job "${claim.jobName}" is not registered`), context);
|
|
@@ -1716,7 +1734,7 @@ class SupaCloudWorker {
|
|
|
1716
1734
|
if (this.stateValue !== "running" && this.stateValue !== "stopping") {
|
|
1717
1735
|
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker is not active");
|
|
1718
1736
|
}
|
|
1719
|
-
const requestContext = claim.requestContext ?? await this.requestContextFactory(claim,
|
|
1737
|
+
const requestContext = claim.requestContext ?? await this.requestContextFactory(claim, signal);
|
|
1720
1738
|
const moduleServices = this.moduleServices.get(entry.module.name);
|
|
1721
1739
|
if (!moduleServices)
|
|
1722
1740
|
throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker services are unavailable");
|
|
@@ -1776,10 +1794,12 @@ class SupaCloudWorker {
|
|
|
1776
1794
|
await Promise.race(this.inFlight).catch((error) => this.reportError(error));
|
|
1777
1795
|
continue;
|
|
1778
1796
|
}
|
|
1797
|
+
const complete = this.work.add({ name: "worker.claim", kind: "job" });
|
|
1779
1798
|
let claim;
|
|
1780
1799
|
try {
|
|
1781
1800
|
claim = await this.transport.claim(signal);
|
|
1782
1801
|
} catch (error) {
|
|
1802
|
+
complete();
|
|
1783
1803
|
if (signal.aborted)
|
|
1784
1804
|
break;
|
|
1785
1805
|
await this.reportError(error);
|
|
@@ -1789,12 +1809,13 @@ class SupaCloudWorker {
|
|
|
1789
1809
|
continue;
|
|
1790
1810
|
}
|
|
1791
1811
|
if (claim === null) {
|
|
1812
|
+
complete();
|
|
1792
1813
|
await delay(this.pollIntervalMs, signal).catch(() => {
|
|
1793
1814
|
return;
|
|
1794
1815
|
});
|
|
1795
1816
|
continue;
|
|
1796
1817
|
}
|
|
1797
|
-
const execution = this.
|
|
1818
|
+
const execution = this.executeClaim(claim, this.work.signal).catch((error) => this.reportError(error)).finally(complete);
|
|
1798
1819
|
this.inFlight.add(execution);
|
|
1799
1820
|
execution.finally(() => this.inFlight.delete(execution));
|
|
1800
1821
|
}
|
|
@@ -2293,7 +2314,7 @@ function assertDeclaredResponseStatus(route, value, configuredStatus) {
|
|
|
2293
2314
|
return;
|
|
2294
2315
|
throw new ApplicationError("Response validation failed", { status: 500, code: "RESPONSE_CONTRACT_UNDECLARED" });
|
|
2295
2316
|
}
|
|
2296
|
-
function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
|
|
2317
|
+
function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}, pendingWork) {
|
|
2297
2318
|
const supportedMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
2298
2319
|
const supportedFields = new Set([
|
|
2299
2320
|
"method",
|
|
@@ -2352,29 +2373,35 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
|
|
|
2352
2373
|
}
|
|
2353
2374
|
}
|
|
2354
2375
|
const requestContexts = new WeakMap;
|
|
2376
|
+
const requestCompletions = new WeakMap;
|
|
2355
2377
|
const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
|
|
2356
2378
|
const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
|
|
2357
2379
|
const plugin = mountHttp(options.http, `supacloud:${compiled.name}`, options.normalize ?? true).derive(async (context) => {
|
|
2358
|
-
|
|
2380
|
+
if (pendingWork?.closed)
|
|
2381
|
+
throw new ApplicationError("Application is stopping", { status: 503, code: "APPLICATION_STOPPING" });
|
|
2382
|
+
const complete = pendingWork?.add({ name: "http.request", kind: "request" });
|
|
2383
|
+
if (complete)
|
|
2384
|
+
requestCompletions.set(context.request, complete);
|
|
2385
|
+
const signal = pendingWork ? AbortSignal.any([context.request.signal, pendingWork.signal]) : context.request.signal;
|
|
2386
|
+
const requestContext = await ctxFactory(context.request, context, signal);
|
|
2359
2387
|
requestContexts.set(context.request, requestContext);
|
|
2360
2388
|
return { services, requestContext };
|
|
2361
2389
|
});
|
|
2362
2390
|
const createRequestScope = compiled.createRequestScope;
|
|
2363
2391
|
const requestScopes = new WeakMap;
|
|
2364
|
-
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
if (
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
}
|
|
2392
|
+
plugin.afterResponse(async ({ request }) => {
|
|
2393
|
+
const scope = requestScopes.get(request);
|
|
2394
|
+
requestScopes.delete(request);
|
|
2395
|
+
try {
|
|
2396
|
+
if (scope)
|
|
2397
|
+
await compiled.destroyRequestScope?.(scope);
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
console.error(`supacloud: request scope cleanup failed for "${compiled.name}"`, error);
|
|
2400
|
+
} finally {
|
|
2401
|
+
requestCompletions.get(request)?.();
|
|
2402
|
+
requestCompletions.delete(request);
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2378
2405
|
plugin.error(async ({ error, request }) => {
|
|
2379
2406
|
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code.toUpperCase().replaceAll("-", "_") : "UNKNOWN";
|
|
2380
2407
|
const context = {
|
|
@@ -2682,16 +2709,17 @@ function isPublicApplicationError(error) {
|
|
|
2682
2709
|
return error.expose === true && typeof error.status === "number" && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 && typeof error.code === "string" && error.code.length > 0;
|
|
2683
2710
|
}
|
|
2684
2711
|
function createApplication(options) {
|
|
2712
|
+
const pendingWork = new PendingWorkRegistry2(options.pendingWork ? { capacity: options.pendingWork.capacity } : {});
|
|
2685
2713
|
const app = new Elysia3({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true }).use(contextPlugin(options.http).as("global"));
|
|
2686
2714
|
if (options.documentation)
|
|
2687
2715
|
app.use(createDocumentationPlugin(options.documentation));
|
|
2688
2716
|
const configuredContextFactory = options.requestContext ?? defaultRequestContext;
|
|
2689
2717
|
const contextCache = new WeakMap;
|
|
2690
|
-
const ctxFactory = (request, context) => {
|
|
2718
|
+
const ctxFactory = (request, context, signal) => {
|
|
2691
2719
|
const cached = contextCache.get(request);
|
|
2692
2720
|
if (cached)
|
|
2693
2721
|
return cached;
|
|
2694
|
-
const pending = Promise.resolve(configuredContextFactory(request, context));
|
|
2722
|
+
const pending = Promise.resolve().then(() => configuredContextFactory(request, context, signal));
|
|
2695
2723
|
contextCache.set(request, pending);
|
|
2696
2724
|
return pending;
|
|
2697
2725
|
};
|
|
@@ -2701,7 +2729,7 @@ function createApplication(options) {
|
|
|
2701
2729
|
const services = module.createServices(options.deps ?? {}, imported);
|
|
2702
2730
|
imported[module.name] = services;
|
|
2703
2731
|
moduleServices.push({ module, services });
|
|
2704
|
-
app.use(createModulePlugin(module, services, ctxFactory, { ...options, http: undefined }, imported));
|
|
2732
|
+
app.use(createModulePlugin(module, services, ctxFactory, { ...options, http: undefined }, imported, pendingWork));
|
|
2705
2733
|
}
|
|
2706
2734
|
let initialized = false;
|
|
2707
2735
|
let destroyed = false;
|
|
@@ -2714,34 +2742,47 @@ function createApplication(options) {
|
|
|
2714
2742
|
get destroyed() {
|
|
2715
2743
|
return destroyed;
|
|
2716
2744
|
},
|
|
2745
|
+
pendingWork,
|
|
2746
|
+
waitForIdle(options) {
|
|
2747
|
+
return pendingWork.waitForIdle(options);
|
|
2748
|
+
},
|
|
2717
2749
|
initialize() {
|
|
2718
2750
|
if (destroyed)
|
|
2719
2751
|
return Promise.reject(new Error("Application has already been destroyed"));
|
|
2720
2752
|
if (initializationPromise)
|
|
2721
2753
|
return initializationPromise;
|
|
2722
|
-
|
|
2754
|
+
const startup = pendingWork.run({ name: "application.startup", kind: "startup" }, () => Promise.resolve().then(async () => {
|
|
2755
|
+
for (const { module, services } of moduleServices) {
|
|
2756
|
+
await module.initializeServices?.(services);
|
|
2757
|
+
}
|
|
2758
|
+
initialized = !destroyed;
|
|
2759
|
+
})).catch(async (error) => {
|
|
2723
2760
|
try {
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
initialized = true;
|
|
2728
|
-
} catch (error) {
|
|
2729
|
-
try {
|
|
2730
|
-
await lifecycle.destroy();
|
|
2731
|
-
} catch (cleanupError) {
|
|
2732
|
-
throw new AggregateError([error, cleanupError], "Application initialization failed and cleanup failed");
|
|
2733
|
-
}
|
|
2734
|
-
throw error;
|
|
2761
|
+
await lifecycle.destroy();
|
|
2762
|
+
} catch (cleanupError) {
|
|
2763
|
+
throw new AggregateError([error, cleanupError], "Application initialization failed and cleanup failed");
|
|
2735
2764
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2765
|
+
throw error;
|
|
2766
|
+
});
|
|
2767
|
+
initializationPromise = startup;
|
|
2768
|
+
return startup;
|
|
2738
2769
|
},
|
|
2739
2770
|
destroy() {
|
|
2740
2771
|
if (destructionPromise)
|
|
2741
2772
|
return destructionPromise;
|
|
2742
2773
|
destroyed = true;
|
|
2743
|
-
|
|
2774
|
+
pendingWork.close();
|
|
2775
|
+
destructionPromise = Promise.resolve().then(async () => {
|
|
2776
|
+
pendingWork.dispose();
|
|
2744
2777
|
const errors = [];
|
|
2778
|
+
try {
|
|
2779
|
+
await pendingWork.waitForIdle({ timeoutMs: options.pendingWork?.shutdownTimeoutMs ?? 5000 });
|
|
2780
|
+
} catch (error) {
|
|
2781
|
+
if (error instanceof PendingWorkTimeoutError)
|
|
2782
|
+
destructionPromise = undefined;
|
|
2783
|
+
throw error;
|
|
2784
|
+
}
|
|
2785
|
+
pendingWork.dispose();
|
|
2745
2786
|
for (const { module, services } of [...moduleServices].reverse()) {
|
|
2746
2787
|
try {
|
|
2747
2788
|
await module.destroyServices?.(services);
|
|
@@ -2752,13 +2793,15 @@ function createApplication(options) {
|
|
|
2752
2793
|
if (errors.length > 0) {
|
|
2753
2794
|
throw new AggregateError(errors, "Application destruction failed");
|
|
2754
2795
|
}
|
|
2755
|
-
})
|
|
2796
|
+
});
|
|
2756
2797
|
return destructionPromise;
|
|
2757
2798
|
}
|
|
2758
2799
|
};
|
|
2759
2800
|
Object.defineProperties(app, {
|
|
2760
2801
|
initialized: { enumerable: true, configurable: false, get: () => lifecycle.initialized },
|
|
2761
2802
|
destroyed: { enumerable: true, configurable: false, get: () => lifecycle.destroyed },
|
|
2803
|
+
pendingWork: { enumerable: true, configurable: false, value: pendingWork },
|
|
2804
|
+
waitForIdle: { enumerable: true, configurable: false, value: lifecycle.waitForIdle },
|
|
2762
2805
|
initialize: { enumerable: true, configurable: false, value: lifecycle.initialize },
|
|
2763
2806
|
destroy: { enumerable: true, configurable: false, value: lifecycle.destroy }
|
|
2764
2807
|
});
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PendingWorkRegistry } from "@supacloud/app/runtime";
|
|
1
2
|
import { type CompiledJob, type CompiledModule, type ExecutionObserver, type JobExecutor } from "./index";
|
|
2
3
|
export type WorkerState = "idle" | "starting" | "running" | "stopping" | "stopped";
|
|
3
4
|
/** The transport-specific claim is mapped to this small execution contract. */
|
|
@@ -76,6 +77,8 @@ export interface WorkerOptions<TClaim = WorkerClaim, TReceipt = unknown> {
|
|
|
76
77
|
workerId?: string;
|
|
77
78
|
concurrency?: number;
|
|
78
79
|
pollIntervalMs?: number;
|
|
80
|
+
/** Maximum drain observation; timeout keeps services owned until a later stop succeeds. */
|
|
81
|
+
shutdownTimeoutMs?: number;
|
|
79
82
|
executor?: JobExecutor;
|
|
80
83
|
onExecution?: ExecutionObserver;
|
|
81
84
|
onError?: (error: unknown) => void | Promise<void>;
|
|
@@ -111,6 +114,9 @@ export declare class SupaCloudWorker<TClaim = WorkerClaim, TReceipt = unknown> {
|
|
|
111
114
|
private readonly workerId;
|
|
112
115
|
private readonly concurrency;
|
|
113
116
|
private readonly pollIntervalMs;
|
|
117
|
+
private readonly shutdownTimeoutMs;
|
|
118
|
+
private work;
|
|
119
|
+
get pendingWork(): PendingWorkRegistry;
|
|
114
120
|
private readonly executor;
|
|
115
121
|
private readonly observer;
|
|
116
122
|
private readonly onError;
|
|
@@ -139,6 +145,7 @@ export declare class SupaCloudWorker<TClaim = WorkerClaim, TReceipt = unknown> {
|
|
|
139
145
|
runOnce(): Promise<WorkerRunResult<TReceipt> | null>;
|
|
140
146
|
/** Process a host-provided claim without performing another claim operation. */
|
|
141
147
|
processClaim(rawClaim: TClaim): Promise<WorkerRunResult<TReceipt>>;
|
|
148
|
+
private executeClaim;
|
|
142
149
|
private failClaim;
|
|
143
150
|
private initialize;
|
|
144
151
|
private releaseServices;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/elysia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,22 +49,22 @@
|
|
|
49
49
|
"directory": "packages/elysia"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@supacloud/app": "0.
|
|
53
|
-
"@supacloud/contracts": "0.
|
|
52
|
+
"@supacloud/app": "0.21.0",
|
|
53
|
+
"@supacloud/contracts": "0.8.0",
|
|
54
54
|
"exact-mirror": "1.2.6",
|
|
55
55
|
"jose": "^6.2.11",
|
|
56
56
|
"typebox": "1.3.34"
|
|
57
57
|
},
|
|
58
58
|
"overrides": {
|
|
59
59
|
"@supacloud/delivery": "0.4.0",
|
|
60
|
-
"@supacloud/contracts": "0.
|
|
60
|
+
"@supacloud/contracts": "0.8.0",
|
|
61
61
|
"@supacloud/commands": "0.9.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"elysia": "2.0.0-beta.19"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@supacloud/compiler": "0.
|
|
67
|
+
"@supacloud/compiler": "0.30.0",
|
|
68
68
|
"@supacloud/delivery": "0.4.0",
|
|
69
69
|
"@supacloud/commands": "0.9.0",
|
|
70
70
|
"@supacloud/db": "0.11.0",
|