@venizia/ignis-worker 0.2.0-2
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/cjs/applications/index.d.ts +2 -0
- package/dist/cjs/applications/index.d.ts.map +1 -0
- package/dist/cjs/applications/index.js +18 -0
- package/dist/cjs/applications/index.js.map +1 -0
- package/dist/cjs/applications/worker.d.ts +67 -0
- package/dist/cjs/applications/worker.d.ts.map +1 -0
- package/dist/cjs/applications/worker.js +175 -0
- package/dist/cjs/applications/worker.js.map +1 -0
- package/dist/cjs/envelope/encode.d.ts +61 -0
- package/dist/cjs/envelope/encode.d.ts.map +1 -0
- package/dist/cjs/envelope/encode.js +116 -0
- package/dist/cjs/envelope/encode.js.map +1 -0
- package/dist/cjs/envelope/index.d.ts +3 -0
- package/dist/cjs/envelope/index.d.ts.map +1 -0
- package/dist/cjs/envelope/index.js +19 -0
- package/dist/cjs/envelope/index.js.map +1 -0
- package/dist/cjs/envelope/types.d.ts +37 -0
- package/dist/cjs/envelope/types.d.ts.map +1 -0
- package/dist/cjs/envelope/types.js +3 -0
- package/dist/cjs/envelope/types.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/transport/common/index.d.ts +2 -0
- package/dist/cjs/transport/common/index.d.ts.map +1 -0
- package/dist/cjs/transport/common/index.js +18 -0
- package/dist/cjs/transport/common/index.js.map +1 -0
- package/dist/cjs/transport/common/types.d.ts +6 -0
- package/dist/cjs/transport/common/types.d.ts.map +1 -0
- package/dist/cjs/transport/common/types.js +3 -0
- package/dist/cjs/transport/common/types.js.map +1 -0
- package/dist/cjs/transport/in-process.d.ts +22 -0
- package/dist/cjs/transport/in-process.d.ts.map +1 -0
- package/dist/cjs/transport/in-process.js +45 -0
- package/dist/cjs/transport/in-process.js.map +1 -0
- package/dist/cjs/transport/index.d.ts +4 -0
- package/dist/cjs/transport/index.d.ts.map +1 -0
- package/dist/cjs/transport/index.js +20 -0
- package/dist/cjs/transport/index.js.map +1 -0
- package/dist/cjs/transport/worker.d.ts +48 -0
- package/dist/cjs/transport/worker.d.ts.map +1 -0
- package/dist/cjs/transport/worker.js +165 -0
- package/dist/cjs/transport/worker.js.map +1 -0
- package/dist/esm/applications/index.d.ts +2 -0
- package/dist/esm/applications/index.d.ts.map +1 -0
- package/dist/esm/applications/index.js +2 -0
- package/dist/esm/applications/index.js.map +1 -0
- package/dist/esm/applications/worker.d.ts +67 -0
- package/dist/esm/applications/worker.d.ts.map +1 -0
- package/dist/esm/applications/worker.js +171 -0
- package/dist/esm/applications/worker.js.map +1 -0
- package/dist/esm/envelope/encode.d.ts +61 -0
- package/dist/esm/envelope/encode.d.ts.map +1 -0
- package/dist/esm/envelope/encode.js +112 -0
- package/dist/esm/envelope/encode.js.map +1 -0
- package/dist/esm/envelope/index.d.ts +3 -0
- package/dist/esm/envelope/index.d.ts.map +1 -0
- package/dist/esm/envelope/index.js +3 -0
- package/dist/esm/envelope/index.js.map +1 -0
- package/dist/esm/envelope/types.d.ts +37 -0
- package/dist/esm/envelope/types.d.ts.map +1 -0
- package/dist/esm/envelope/types.js +2 -0
- package/dist/esm/envelope/types.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/transport/common/index.d.ts +2 -0
- package/dist/esm/transport/common/index.d.ts.map +1 -0
- package/dist/esm/transport/common/index.js +2 -0
- package/dist/esm/transport/common/index.js.map +1 -0
- package/dist/esm/transport/common/types.d.ts +6 -0
- package/dist/esm/transport/common/types.d.ts.map +1 -0
- package/dist/esm/transport/common/types.js +2 -0
- package/dist/esm/transport/common/types.js.map +1 -0
- package/dist/esm/transport/in-process.d.ts +22 -0
- package/dist/esm/transport/in-process.d.ts.map +1 -0
- package/dist/esm/transport/in-process.js +41 -0
- package/dist/esm/transport/in-process.js.map +1 -0
- package/dist/esm/transport/index.d.ts +4 -0
- package/dist/esm/transport/index.d.ts.map +1 -0
- package/dist/esm/transport/index.js +4 -0
- package/dist/esm/transport/index.js.map +1 -0
- package/dist/esm/transport/worker.d.ts +48 -0
- package/dist/esm/transport/worker.d.ts.map +1 -0
- package/dist/esm/transport/worker.js +161 -0
- package/dist/esm/transport/worker.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/transport/common/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IBffTransport } from './common/types';
|
|
2
|
+
/**
|
|
3
|
+
* No worker, no browser - the seam that makes a BFF testable in-process. It still runs the full
|
|
4
|
+
* envelope round trip, and that is the whole point: a bare pass-through lets a route pass here and
|
|
5
|
+
* hang over a real Worker, because only the envelope applies the synthetic origin, buffers a
|
|
6
|
+
* streaming body, and reduces a thrown error to `{ text, code, args }`. A test is worth what its
|
|
7
|
+
* differences from production are worth.
|
|
8
|
+
*
|
|
9
|
+
* What it deliberately does NOT mirror is the timeout: in-process there is no message that can be
|
|
10
|
+
* lost, so a handler either settles or the caller's own test timeout fires.
|
|
11
|
+
*/
|
|
12
|
+
export declare class InProcessBffTransport implements IBffTransport {
|
|
13
|
+
private readonly handler;
|
|
14
|
+
private readonly requestIdGenerator;
|
|
15
|
+
constructor(opts: {
|
|
16
|
+
handler: (request: Request) => Promise<Response>;
|
|
17
|
+
});
|
|
18
|
+
fetch(opts: {
|
|
19
|
+
request: Request;
|
|
20
|
+
}): Promise<Response>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=in-process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process.d.ts","sourceRoot":"","sources":["../../../src/transport/in-process.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;;;;;;;;GASG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;IAClE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAEhC;gBAES,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE;IAIhE,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;CAuB3D"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InProcessBffTransport = void 0;
|
|
4
|
+
const core_1 = require("@venizia/ignis-helpers/core");
|
|
5
|
+
const encode_1 = require("../envelope/encode");
|
|
6
|
+
/**
|
|
7
|
+
* No worker, no browser - the seam that makes a BFF testable in-process. It still runs the full
|
|
8
|
+
* envelope round trip, and that is the whole point: a bare pass-through lets a route pass here and
|
|
9
|
+
* hang over a real Worker, because only the envelope applies the synthetic origin, buffers a
|
|
10
|
+
* streaming body, and reduces a thrown error to `{ text, code, args }`. A test is worth what its
|
|
11
|
+
* differences from production are worth.
|
|
12
|
+
*
|
|
13
|
+
* What it deliberately does NOT mirror is the timeout: in-process there is no message that can be
|
|
14
|
+
* lost, so a handler either settles or the caller's own test timeout fires.
|
|
15
|
+
*/
|
|
16
|
+
class InProcessBffTransport {
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.requestIdGenerator = new core_1.RequestIdGenerator({
|
|
19
|
+
scope: InProcessBffTransport.name,
|
|
20
|
+
});
|
|
21
|
+
this.handler = opts.handler;
|
|
22
|
+
}
|
|
23
|
+
async fetch(opts) {
|
|
24
|
+
const id = this.requestIdGenerator.nextId();
|
|
25
|
+
const requestEnvelope = await encode_1.BffEnvelope.encodeRequest({
|
|
26
|
+
request: opts.request,
|
|
27
|
+
id,
|
|
28
|
+
url: encode_1.BffEnvelope.toSyntheticUrl({ url: opts.request.url }),
|
|
29
|
+
});
|
|
30
|
+
let responseEnvelope;
|
|
31
|
+
try {
|
|
32
|
+
const response = await this.handler(encode_1.BffEnvelope.decodeRequest({ envelope: requestEnvelope }));
|
|
33
|
+
responseEnvelope = await encode_1.BffEnvelope.encodeResponse({ response, id });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// Encoded and decoded rather than rethrown: what a UI receives over a real Worker is a
|
|
37
|
+
// rebuilt `ApplicationError` carrying only the normalised shape - never the original object,
|
|
38
|
+
// its stack or its `extra`.
|
|
39
|
+
throw encode_1.BffEnvelope.decodeError({ envelope: encode_1.BffEnvelope.encodeError({ id, error }) });
|
|
40
|
+
}
|
|
41
|
+
return encode_1.BffEnvelope.decodeResponse({ envelope: responseEnvelope });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.InProcessBffTransport = InProcessBffTransport;
|
|
45
|
+
//# sourceMappingURL=in-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process.js","sourceRoot":"","sources":["../../../src/transport/in-process.ts"],"names":[],"mappings":";;;AAAA,sDAAiE;AACjE,8CAAgD;AAIhD;;;;;;;;;GASG;AACH,MAAa,qBAAqB;IAMhC,YAAY,IAA0D;QAJrD,uBAAkB,GAAG,IAAI,yBAAkB,CAAC;YAC3D,KAAK,EAAE,qBAAqB,CAAC,IAAI;SAClC,CAAC,CAAC;QAGD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAA0B;QACpC,MAAM,EAAE,GAAG,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,CAAC;QAE5C,MAAM,eAAe,GAAG,MAAM,oBAAW,CAAC,aAAa,CAAC;YACtD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,EAAE;YACF,GAAG,EAAE,oBAAW,CAAC,cAAc,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;SAC3D,CAAC,CAAC;QAEH,IAAI,gBAAsC,CAAC;QAE3C,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,oBAAW,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC;YAC9F,gBAAgB,GAAG,MAAM,oBAAW,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,uFAAuF;YACvF,6FAA6F;YAC7F,4BAA4B;YAC5B,MAAM,oBAAW,CAAC,WAAW,CAAC,EAAE,QAAQ,EAAE,oBAAW,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QAED,OAAO,oBAAW,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACpE,CAAC;CACF;AAjCD,sDAiCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./common"), exports);
|
|
18
|
+
__exportStar(require("./in-process"), exports);
|
|
19
|
+
__exportStar(require("./worker"), exports);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,+CAA6B;AAC7B,2CAAyB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseHelper } from '@venizia/ignis-helpers/core';
|
|
2
|
+
import type { IBffTransport } from './common/types';
|
|
3
|
+
/**
|
|
4
|
+
* UI-side half of the BFF: posts an encoded request to a real `Worker` and resolves the matching
|
|
5
|
+
* response by `id` - the far side of `WorkerApplication.listen()`.
|
|
6
|
+
*
|
|
7
|
+
* The three worker listeners are attached ONCE, in the constructor, and every request is tracked in
|
|
8
|
+
* `pendingRequests` instead. Registering them per request looks equivalent and is not: they are
|
|
9
|
+
* scoped to the shared `Worker`, so a single `messageerror` - which means exactly one message
|
|
10
|
+
* failed to deserialise - would fire every in-flight request's handler, rejecting nine healthy
|
|
11
|
+
* requests that are about to be answered normally.
|
|
12
|
+
*/
|
|
13
|
+
export declare class WorkerBffTransport extends BaseHelper implements IBffTransport {
|
|
14
|
+
private readonly worker;
|
|
15
|
+
private readonly timeoutMs;
|
|
16
|
+
private readonly requestIdGenerator;
|
|
17
|
+
private readonly pendingRequests;
|
|
18
|
+
constructor(opts: {
|
|
19
|
+
worker: Worker;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
});
|
|
22
|
+
/** Removes a pending request from the map and releases its timer and abort listener, so nothing can settle it twice. */
|
|
23
|
+
private readonly takePending;
|
|
24
|
+
private readonly handleMessage;
|
|
25
|
+
/**
|
|
26
|
+
* A dead or crashed worker never posts anything back, so every in-flight request is lost - this
|
|
27
|
+
* is the one event that legitimately settles all of them, instead of leaving each to time out.
|
|
28
|
+
*/
|
|
29
|
+
private readonly handleWorkerError;
|
|
30
|
+
/**
|
|
31
|
+
* `messageerror` means exactly ONE posted message failed to deserialise, and the event carries
|
|
32
|
+
* nothing that says which. With a single request in flight that is unambiguous, so it settles
|
|
33
|
+
* now; with several, the lost answer belongs to one of them and rejecting the rest would be a
|
|
34
|
+
* lie - each then ends on its own timeout, under its own id.
|
|
35
|
+
*/
|
|
36
|
+
private readonly handleMessageError;
|
|
37
|
+
fetch(opts: {
|
|
38
|
+
request: Request;
|
|
39
|
+
}): Promise<Response>;
|
|
40
|
+
/** `signal.reason` first, the way `globalThis.fetch` rejects, so `controller.abort(myReason)` still reaches the caller. */
|
|
41
|
+
private toAbortError;
|
|
42
|
+
/**
|
|
43
|
+
* Detaches the three worker listeners and fails whatever is still in flight. The worker itself is
|
|
44
|
+
* left alone - this transport did not create it, and a page may hand the same one to another.
|
|
45
|
+
*/
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/transport/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAY,MAAM,6BAA6B,CAAC;AAInE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAiBpD;;;;;;;;;GASG;AACH,qBAAa,kBAAmB,SAAQ,UAAW,YAAW,aAAa;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAEhC;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;gBAE1D,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;IAWxD,wHAAwH;IACxH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAS1B;IAEF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAgC5B;IAEF;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAQhC;IAEF;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAkBjC;IAEI,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqD1D,2HAA2H;IAC3H,OAAO,CAAC,YAAY;IAOpB;;;OAGG;IACH,KAAK,IAAI,IAAI;CAad"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkerBffTransport = void 0;
|
|
4
|
+
const core_1 = require("@venizia/ignis-helpers/core");
|
|
5
|
+
const core_2 = require("@venizia/ignis-helpers/core");
|
|
6
|
+
const encode_1 = require("../envelope/encode");
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const isErrorEnvelope = (data) => {
|
|
9
|
+
return 'error' in data;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* UI-side half of the BFF: posts an encoded request to a real `Worker` and resolves the matching
|
|
13
|
+
* response by `id` - the far side of `WorkerApplication.listen()`.
|
|
14
|
+
*
|
|
15
|
+
* The three worker listeners are attached ONCE, in the constructor, and every request is tracked in
|
|
16
|
+
* `pendingRequests` instead. Registering them per request looks equivalent and is not: they are
|
|
17
|
+
* scoped to the shared `Worker`, so a single `messageerror` - which means exactly one message
|
|
18
|
+
* failed to deserialise - would fire every in-flight request's handler, rejecting nine healthy
|
|
19
|
+
* requests that are about to be answered normally.
|
|
20
|
+
*/
|
|
21
|
+
class WorkerBffTransport extends core_1.BaseHelper {
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
super({ scope: WorkerBffTransport.name });
|
|
24
|
+
this.requestIdGenerator = new core_2.RequestIdGenerator({
|
|
25
|
+
scope: WorkerBffTransport.name,
|
|
26
|
+
});
|
|
27
|
+
this.pendingRequests = new Map();
|
|
28
|
+
/** Removes a pending request from the map and releases its timer and abort listener, so nothing can settle it twice. */
|
|
29
|
+
this.takePending = (opts) => {
|
|
30
|
+
const pending = this.pendingRequests.get(opts.id);
|
|
31
|
+
if (!pending) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
this.pendingRequests.delete(opts.id);
|
|
35
|
+
pending.release();
|
|
36
|
+
return pending;
|
|
37
|
+
};
|
|
38
|
+
this.handleMessage = (event) => {
|
|
39
|
+
// `event.data` is untyped (`any`) until narrowed below - a worker posts to this page for its own
|
|
40
|
+
// reasons too (a dev-server ping, a library handshake), and none of those carry an `id` this
|
|
41
|
+
// transport is waiting on.
|
|
42
|
+
const data = event.data;
|
|
43
|
+
if (!data?.id) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const pending = this.takePending({ id: data.id });
|
|
47
|
+
if (!pending) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (isErrorEnvelope(data)) {
|
|
51
|
+
pending.reject(encode_1.BffEnvelope.decodeError({ envelope: data }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Settled inside its own try: `decodeResponse` throws a `RangeError` for a status outside
|
|
55
|
+
// 200-599 and a `TypeError` on a malformed header tuple, and by this point the timer and the
|
|
56
|
+
// listeners are already gone - a throw that escaped here would leave the promise with nothing
|
|
57
|
+
// left to settle it, ever.
|
|
58
|
+
try {
|
|
59
|
+
pending.resolve(encode_1.BffEnvelope.decodeResponse({ envelope: data }));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
pending.reject((0, core_1.getError)({
|
|
63
|
+
message: `[WorkerBffTransport] Failed to decode the response envelope | id: ${data.id} | error: ${error}`,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A dead or crashed worker never posts anything back, so every in-flight request is lost - this
|
|
69
|
+
* is the one event that legitimately settles all of them, instead of leaving each to time out.
|
|
70
|
+
*/
|
|
71
|
+
this.handleWorkerError = (event) => {
|
|
72
|
+
for (const id of [...this.pendingRequests.keys()]) {
|
|
73
|
+
this.takePending({ id })?.reject((0, core_1.getError)({
|
|
74
|
+
message: `[WorkerBffTransport] Worker failed while awaiting a response | id: ${id} | event: ${String(event)}`,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* `messageerror` means exactly ONE posted message failed to deserialise, and the event carries
|
|
80
|
+
* nothing that says which. With a single request in flight that is unambiguous, so it settles
|
|
81
|
+
* now; with several, the lost answer belongs to one of them and rejecting the rest would be a
|
|
82
|
+
* lie - each then ends on its own timeout, under its own id.
|
|
83
|
+
*/
|
|
84
|
+
this.handleMessageError = (event) => {
|
|
85
|
+
if (this.pendingRequests.size !== 1) {
|
|
86
|
+
this.logger
|
|
87
|
+
.for(this.handleMessageError.name)
|
|
88
|
+
.warn('A worker message could not be deserialised and no single request owns it | inFlight: %d | event: %s', this.pendingRequests.size, String(event));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const [id] = [...this.pendingRequests.keys()];
|
|
92
|
+
this.takePending({ id: id })?.reject((0, core_1.getError)({
|
|
93
|
+
message: `[WorkerBffTransport] The worker's response could not be deserialised | id: ${id} | event: ${String(event)}`,
|
|
94
|
+
}));
|
|
95
|
+
};
|
|
96
|
+
this.worker = opts.worker;
|
|
97
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
98
|
+
this.worker.addEventListener('message', this.handleMessage);
|
|
99
|
+
this.worker.addEventListener('error', this.handleWorkerError);
|
|
100
|
+
this.worker.addEventListener('messageerror', this.handleMessageError);
|
|
101
|
+
}
|
|
102
|
+
async fetch(opts) {
|
|
103
|
+
const { request } = opts;
|
|
104
|
+
const { signal } = request;
|
|
105
|
+
if (signal?.aborted) {
|
|
106
|
+
throw this.toAbortError({ signal });
|
|
107
|
+
}
|
|
108
|
+
const id = this.requestIdGenerator.nextId();
|
|
109
|
+
// The URL is overridden rather than the request rebuilt: rebuilding reads the body once here
|
|
110
|
+
// and once inside `encodeRequest`, and it silently drops `signal` on the way.
|
|
111
|
+
const requestEnvelope = await encode_1.BffEnvelope.encodeRequest({
|
|
112
|
+
request,
|
|
113
|
+
id,
|
|
114
|
+
url: encode_1.BffEnvelope.toSyntheticUrl({ url: request.url }),
|
|
115
|
+
});
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
this.takePending({ id })?.reject((0, core_1.getError)({
|
|
119
|
+
message: `[WorkerBffTransport] Timed out waiting for a response | id: ${id} | timeoutMs: ${this.timeoutMs}`,
|
|
120
|
+
}));
|
|
121
|
+
}, this.timeoutMs);
|
|
122
|
+
const handleAbort = () => {
|
|
123
|
+
this.takePending({ id })?.reject(this.toAbortError({ signal }));
|
|
124
|
+
};
|
|
125
|
+
signal?.addEventListener('abort', handleAbort);
|
|
126
|
+
this.pendingRequests.set(id, {
|
|
127
|
+
resolve,
|
|
128
|
+
reject,
|
|
129
|
+
release: () => {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
signal?.removeEventListener('abort', handleAbort);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Buffering the body took an await, and `addEventListener('abort')` never fires on a signal
|
|
135
|
+
// that aborted before it was attached - without this re-read that window loses the abort and
|
|
136
|
+
// the caller waits out the whole timeout.
|
|
137
|
+
if (signal?.aborted) {
|
|
138
|
+
handleAbort();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.worker.postMessage(requestEnvelope, requestEnvelope.body ? [requestEnvelope.body] : []);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** `signal.reason` first, the way `globalThis.fetch` rejects, so `controller.abort(myReason)` still reaches the caller. */
|
|
145
|
+
toAbortError(opts) {
|
|
146
|
+
return (opts.signal?.reason ??
|
|
147
|
+
(0, core_1.getError)({ message: '[WorkerBffTransport] The request was aborted by its caller' }));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Detaches the three worker listeners and fails whatever is still in flight. The worker itself is
|
|
151
|
+
* left alone - this transport did not create it, and a page may hand the same one to another.
|
|
152
|
+
*/
|
|
153
|
+
close() {
|
|
154
|
+
this.worker.removeEventListener('message', this.handleMessage);
|
|
155
|
+
this.worker.removeEventListener('error', this.handleWorkerError);
|
|
156
|
+
this.worker.removeEventListener('messageerror', this.handleMessageError);
|
|
157
|
+
for (const id of [...this.pendingRequests.keys()]) {
|
|
158
|
+
this.takePending({ id })?.reject((0, core_1.getError)({
|
|
159
|
+
message: `[WorkerBffTransport] The transport was closed while awaiting a response | id: ${id}`,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.WorkerBffTransport = WorkerBffTransport;
|
|
165
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/transport/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/applications/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/applications/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RestApplication } from '@venizia/ignis-kernel';
|
|
2
|
+
import type { Env, Schema } from 'hono';
|
|
3
|
+
/**
|
|
4
|
+
* Structural shape of the two globals `listen()` can attach to. Never `DedicatedWorkerGlobalScope`:
|
|
5
|
+
* naming it emits it into the published `.d.ts` and forces every consumer to compile with
|
|
6
|
+
* `lib.webworker`. `start` is optional because a `MessagePort`s queue starts disabled and only
|
|
7
|
+
* `start()` enables it, while a worker global scope has no such method at all.
|
|
8
|
+
*/
|
|
9
|
+
export interface IWorkerMessageScope {
|
|
10
|
+
postMessage(message: unknown, transfer?: Array<ArrayBuffer | MessagePort>): void;
|
|
11
|
+
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
12
|
+
removeEventListener?(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
13
|
+
start?(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs an IGNIS application inside a browser Web Worker: `listen()` replaces what
|
|
17
|
+
* `ServerApplication.start()` does with a socket, attaching `onmessage` instead. Gets REST
|
|
18
|
+
* controller registration by inheritance - `RestApplication.registerControllers()` - the whole
|
|
19
|
+
* reason the DI helpers and `RestComponent` sit in the kernel.
|
|
20
|
+
*/
|
|
21
|
+
export declare abstract class WorkerApplication<AppEnv extends Env = Env, AppSchema extends Schema = {}, BasePath extends string = '/'> extends RestApplication<AppEnv, AppSchema, BasePath> {
|
|
22
|
+
/** Resolves once the router tree is mounted - what an envelope arriving during boot waits on. */
|
|
23
|
+
private servingPromise?;
|
|
24
|
+
/** The whole `listen()` call, kept so a second one is a no-op instead of a second listener. */
|
|
25
|
+
private listenPromise?;
|
|
26
|
+
private detachFromScope?;
|
|
27
|
+
private isStopped;
|
|
28
|
+
/**
|
|
29
|
+
* Every failure here is posted back as an `IBffErrorEnvelope`, the failed `postMessage` included:
|
|
30
|
+
* an exception nobody posts leaves the UI waiting out the transport timeout.
|
|
31
|
+
*/
|
|
32
|
+
private handleMessage;
|
|
33
|
+
/** No transfer list: an error envelope carries no `ArrayBuffer` body, and this is already the fallback path - nothing left to hand off if this post fails too. */
|
|
34
|
+
private postErrorEnvelope;
|
|
35
|
+
/** Everything `ServerApplication.start()` does short of binding a socket. Once this resolves the application answers requests. */
|
|
36
|
+
private startServing;
|
|
37
|
+
/**
|
|
38
|
+
* Holds an envelope until the application is serving. A dedicated worker's queue is enabled when
|
|
39
|
+
* its module script is evaluated, so anything posted during boot - PGlite takes ~350ms - reaches
|
|
40
|
+
* this listener and would otherwise be dropped. One shared promise also hands them on in arrival
|
|
41
|
+
* order.
|
|
42
|
+
*/
|
|
43
|
+
private dispatchMessage;
|
|
44
|
+
private attachTo;
|
|
45
|
+
/**
|
|
46
|
+
* `ServerApplication.start()` minus the socket. `opts.scope` defaults to `globalThis`, never a
|
|
47
|
+
* bare `self`, which would tie the published types to `lib.webworker`. Idempotent: without the
|
|
48
|
+
* guard one envelope runs its handler once per call - a POST inserting two rows - and Vite HMR
|
|
49
|
+
* re-evaluating the worker module is the everyday trigger. A boot failure keeps the listener
|
|
50
|
+
* attached so every request hears the failure instead of waiting out the transport timeout.
|
|
51
|
+
*/
|
|
52
|
+
listen(opts?: {
|
|
53
|
+
scope?: MessagePort | IWorkerMessageScope;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* The counterpart to `listen()`, and to `ServerApplication.stop()`: without it nothing drains the
|
|
57
|
+
* post-stop hooks, so a datasource's `end()` never runs and the OPFS exclusive lock a PGlite
|
|
58
|
+
* holds is never released - the next Worker then fails with `NoModificationAllowedError` until a
|
|
59
|
+
* full page reload. The Vite recipe is `import.meta.hot?.dispose(() => application.stop())`.
|
|
60
|
+
*
|
|
61
|
+
* Detaches BEFORE draining, unlike the server: here the listener IS the socket, and a request
|
|
62
|
+
* admitted while the hooks are closing a datasource has nothing left to run against. A stopped
|
|
63
|
+
* application is not restartable - build a new one.
|
|
64
|
+
*/
|
|
65
|
+
stop(): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/applications/worker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAIxC;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,KAAK,CAAC,WAAW,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC;IACjF,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI,CAAC;IACjF,mBAAmB,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI,CAAC;IACrF,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED;;;;;GAKG;AACH,8BAAsB,iBAAiB,CACrC,MAAM,SAAS,GAAG,GAAG,GAAG,EACxB,SAAS,SAAS,MAAM,GAAG,EAAE,EAC7B,QAAQ,SAAS,MAAM,GAAG,GAAG,CAC7B,SAAQ,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;IACpD,iGAAiG;IACjG,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,+FAA+F;IAC/F,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAC,CAAa;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B;;;OAGG;YACW,aAAa;IAoC3B,kKAAkK;IAClK,OAAO,CAAC,iBAAiB;IAgBzB,kIAAkI;YACpH,YAAY;IAW1B;;;;;OAKG;YACW,eAAe;IAqC7B,OAAO,CAAC,QAAQ;IAsChB;;;;;;OAMG;IACG,MAAM,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,WAAW,GAAG,mBAAmB,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBjF;;;;;;;;;OASG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAU5B"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { getError } from '@venizia/ignis-helpers/core';
|
|
2
|
+
import { HTTP } from '@venizia/ignis-helpers/common';
|
|
3
|
+
import { RestApplication } from '@venizia/ignis-kernel';
|
|
4
|
+
import { BffEnvelope } from '../envelope/encode.js';
|
|
5
|
+
/**
|
|
6
|
+
* Runs an IGNIS application inside a browser Web Worker: `listen()` replaces what
|
|
7
|
+
* `ServerApplication.start()` does with a socket, attaching `onmessage` instead. Gets REST
|
|
8
|
+
* controller registration by inheritance - `RestApplication.registerControllers()` - the whole
|
|
9
|
+
* reason the DI helpers and `RestComponent` sit in the kernel.
|
|
10
|
+
*/
|
|
11
|
+
export class WorkerApplication extends RestApplication {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.isStopped = false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Every failure here is posted back as an `IBffErrorEnvelope`, the failed `postMessage` included:
|
|
18
|
+
* an exception nobody posts leaves the UI waiting out the transport timeout.
|
|
19
|
+
*/
|
|
20
|
+
async handleMessage(opts) {
|
|
21
|
+
const { scope, envelope } = opts;
|
|
22
|
+
const logger = this.logger.for(this.handleMessage.name);
|
|
23
|
+
let responseEnvelope;
|
|
24
|
+
let failure;
|
|
25
|
+
try {
|
|
26
|
+
const request = BffEnvelope.decodeRequest({ envelope });
|
|
27
|
+
const response = await this.getServer().fetch(request);
|
|
28
|
+
responseEnvelope = await BffEnvelope.encodeResponse({ response, id: envelope.id });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger.error('Failed to handle worker message | id: %s | error: %s', envelope.id, error);
|
|
32
|
+
failure = error;
|
|
33
|
+
}
|
|
34
|
+
if (responseEnvelope) {
|
|
35
|
+
try {
|
|
36
|
+
scope.postMessage(responseEnvelope, responseEnvelope.body ? [responseEnvelope.body] : []);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
catch (postError) {
|
|
40
|
+
logger.error('Failed to post response back to scope, falling back to an error envelope | id: %s | error: %s', envelope.id, postError);
|
|
41
|
+
failure = postError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.postErrorEnvelope({ scope, id: envelope.id, error: failure });
|
|
45
|
+
}
|
|
46
|
+
/** No transfer list: an error envelope carries no `ArrayBuffer` body, and this is already the fallback path - nothing left to hand off if this post fails too. */
|
|
47
|
+
postErrorEnvelope(opts) {
|
|
48
|
+
const { scope, id, error } = opts;
|
|
49
|
+
try {
|
|
50
|
+
scope.postMessage(BffEnvelope.encodeError({ id, error }));
|
|
51
|
+
}
|
|
52
|
+
catch (postError) {
|
|
53
|
+
this.logger
|
|
54
|
+
.for(this.postErrorEnvelope.name)
|
|
55
|
+
.error('Failed to post error envelope back to scope | id: %s | error: %s', id, postError);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Everything `ServerApplication.start()` does short of binding a socket. Once this resolves the application answers requests. */
|
|
59
|
+
async startServing() {
|
|
60
|
+
// The stack itself is `RestApplication.registerDefaultMiddlewares()` - a BFF must answer with
|
|
61
|
+
// the framework's envelope exactly as the server does, so it adds nothing and overrides nothing.
|
|
62
|
+
await this.registerDefaultMiddlewares();
|
|
63
|
+
await this.initialize();
|
|
64
|
+
await this.setupMiddlewares();
|
|
65
|
+
this.getServer().route(this.configs.path.base, this.rootRouter);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Holds an envelope until the application is serving. A dedicated worker's queue is enabled when
|
|
69
|
+
* its module script is evaluated, so anything posted during boot - PGlite takes ~350ms - reaches
|
|
70
|
+
* this listener and would otherwise be dropped. One shared promise also hands them on in arrival
|
|
71
|
+
* order.
|
|
72
|
+
*/
|
|
73
|
+
async dispatchMessage(opts) {
|
|
74
|
+
const { scope, envelope } = opts;
|
|
75
|
+
const logger = this.logger.for(this.dispatchMessage.name);
|
|
76
|
+
try {
|
|
77
|
+
await this.servingPromise;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.error('The application failed to start, answering with an error envelope | id: %s | error: %s', envelope.id, error);
|
|
81
|
+
this.postErrorEnvelope({ scope, id: envelope.id, error });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Answered, not dropped: a request admitted before `stop()` and still waiting on the boot has
|
|
85
|
+
// a caller holding a promise, and silence there is the timeout this whole path exists to avoid.
|
|
86
|
+
if (this.isStopped) {
|
|
87
|
+
logger.warn('Refusing a request that arrived after stop() | id: %s', envelope.id);
|
|
88
|
+
this.postErrorEnvelope({
|
|
89
|
+
scope,
|
|
90
|
+
id: envelope.id,
|
|
91
|
+
error: getError({
|
|
92
|
+
statusCode: HTTP.ResultCodes.RS_5.ServiceUnavailable,
|
|
93
|
+
message: `[WorkerApplication] The application was stopped before this request could run | id: ${envelope.id}`,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await this.handleMessage({ scope, envelope });
|
|
99
|
+
}
|
|
100
|
+
attachTo(opts) {
|
|
101
|
+
const { scope } = opts;
|
|
102
|
+
const logger = this.logger.for(this.attachTo.name);
|
|
103
|
+
// Typed as the broader `Event`, not `MessageEvent`: Node's ambient `worker_threads.MessagePort`
|
|
104
|
+
// (the fallback this package's DOM-less tsconfig resolves `MessagePort` to) declares
|
|
105
|
+
// `addEventListener` over `Event | MessageEvent`, and a handler narrower than that is not
|
|
106
|
+
// assignable to it.
|
|
107
|
+
const listener = (event) => {
|
|
108
|
+
const envelope = event.data;
|
|
109
|
+
// A worker scope receives every `postMessage` sent to it, not only ours - a dev-server ping,
|
|
110
|
+
// a devtools/extension probe, a library's own init handshake. None of those carry an `id`;
|
|
111
|
+
// silently dropping them (rather than dereferencing `envelope.id` inside `handleMessage`'s
|
|
112
|
+
// own catch block) is what keeps a foreign message from throwing a bare `TypeError` that
|
|
113
|
+
// would otherwise escape this listener entirely, unlogged and unposted.
|
|
114
|
+
if (!envelope?.id) {
|
|
115
|
+
logger.debug('Ignoring worker message with no request id | data: %j', envelope);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// `dispatchMessage` never rejects - every failure inside it, including a failed post, is
|
|
119
|
+
// caught and handled internally. This `.catch` only guards the sliver of code before its own
|
|
120
|
+
// try block ever runs.
|
|
121
|
+
this.dispatchMessage({ scope, envelope }).catch(error => {
|
|
122
|
+
logger.error('Unexpected failure handling worker message | error: %s', error);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
scope.addEventListener('message', listener);
|
|
126
|
+
scope.start?.();
|
|
127
|
+
this.detachFromScope = () => {
|
|
128
|
+
scope.removeEventListener?.('message', listener);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `ServerApplication.start()` minus the socket. `opts.scope` defaults to `globalThis`, never a
|
|
133
|
+
* bare `self`, which would tie the published types to `lib.webworker`. Idempotent: without the
|
|
134
|
+
* guard one envelope runs its handler once per call - a POST inserting two rows - and Vite HMR
|
|
135
|
+
* re-evaluating the worker module is the everyday trigger. A boot failure keeps the listener
|
|
136
|
+
* attached so every request hears the failure instead of waiting out the transport timeout.
|
|
137
|
+
*/
|
|
138
|
+
async listen(opts) {
|
|
139
|
+
if (this.listenPromise) {
|
|
140
|
+
this.logger
|
|
141
|
+
.for(this.listen.name)
|
|
142
|
+
.warn('Already listening - ignoring this call | one scope, one listener');
|
|
143
|
+
return this.listenPromise;
|
|
144
|
+
}
|
|
145
|
+
// Everything down to the first `await` runs synchronously with the caller, which is what makes
|
|
146
|
+
// the listener observable to anything posted in the same turn.
|
|
147
|
+
const scope = opts?.scope ?? globalThis;
|
|
148
|
+
this.attachTo({ scope });
|
|
149
|
+
this.servingPromise = this.startServing();
|
|
150
|
+
this.listenPromise = this.servingPromise.then(() => this.executePostStartHooks());
|
|
151
|
+
return this.listenPromise;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The counterpart to `listen()`, and to `ServerApplication.stop()`: without it nothing drains the
|
|
155
|
+
* post-stop hooks, so a datasource's `end()` never runs and the OPFS exclusive lock a PGlite
|
|
156
|
+
* holds is never released - the next Worker then fails with `NoModificationAllowedError` until a
|
|
157
|
+
* full page reload. The Vite recipe is `import.meta.hot?.dispose(() => application.stop())`.
|
|
158
|
+
*
|
|
159
|
+
* Detaches BEFORE draining, unlike the server: here the listener IS the socket, and a request
|
|
160
|
+
* admitted while the hooks are closing a datasource has nothing left to run against. A stopped
|
|
161
|
+
* application is not restartable - build a new one.
|
|
162
|
+
*/
|
|
163
|
+
async stop() {
|
|
164
|
+
this.isStopped = true;
|
|
165
|
+
this.detachFromScope?.();
|
|
166
|
+
this.detachFromScope = undefined;
|
|
167
|
+
await this.executePostStopHooks();
|
|
168
|
+
this.logger.for(this.stop.name).info('Worker application STOPPED');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/applications/worker.ts"],"names":[],"mappings":"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"}
|