@noego/wood 0.4.5 → 0.4.7
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/bin/wood.js +9 -1
- package/dist/client/app_env.cjs +39 -0
- package/dist/client/app_env.cjs.map +1 -0
- package/dist/client/app_env.d.cts +7 -0
- package/dist/client/app_env.d.ts +7 -0
- package/dist/client/app_env.js +14 -0
- package/dist/client/app_env.js.map +1 -0
- package/dist/client/index.cjs +33 -9
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +5 -1
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +32 -9
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/channel_map_generator.cjs +102 -0
- package/dist/codegen/channel_map_generator.cjs.map +1 -0
- package/dist/codegen/channel_map_generator.d.cts +6 -0
- package/dist/codegen/channel_map_generator.d.ts +6 -0
- package/dist/codegen/channel_map_generator.js +78 -0
- package/dist/codegen/channel_map_generator.js.map +1 -0
- package/dist/codegen/controller_registration_generator.cjs +6 -1
- package/dist/codegen/controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/controller_registration_generator.js +6 -1
- package/dist/codegen/controller_registration_generator.js.map +1 -1
- package/dist/codegen/generated_file_notice.cjs +40 -0
- package/dist/codegen/generated_file_notice.cjs.map +1 -0
- package/dist/codegen/generated_file_notice.d.cts +3 -0
- package/dist/codegen/generated_file_notice.d.ts +3 -0
- package/dist/codegen/generated_file_notice.js +16 -0
- package/dist/codegen/generated_file_notice.js.map +1 -0
- package/dist/codegen/index.cjs +3 -0
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +1 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/codegen/index.js +2 -0
- package/dist/codegen/index.js.map +1 -1
- package/dist/codegen/preload_generator.cjs +97 -1
- package/dist/codegen/preload_generator.cjs.map +1 -1
- package/dist/codegen/preload_generator.js +97 -1
- package/dist/codegen/preload_generator.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +6 -1
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +6 -1
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.js +6 -1
- package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.js +6 -1
- package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
- package/dist/codegen/runtime_generator.cjs +6 -1
- package/dist/codegen/runtime_generator.cjs.map +1 -1
- package/dist/codegen/runtime_generator.js +6 -1
- package/dist/codegen/runtime_generator.js.map +1 -1
- package/dist/codegen/test_mock_generator.cjs +5 -1
- package/dist/codegen/test_mock_generator.cjs.map +1 -1
- package/dist/codegen/test_mock_generator.js +5 -1
- package/dist/codegen/test_mock_generator.js.map +1 -1
- package/dist/codegen/type_generator.cjs +7 -1
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +7 -1
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/index.cjs +19 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main/index.cjs +287 -62
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +57 -3
- package/dist/main/index.d.ts +57 -3
- package/dist/main/index.js +289 -62
- package/dist/main/index.js.map +1 -1
- package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
- package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
- package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.js +101 -0
- package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
- package/dist/router/wood_router.cjs +9 -1
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +1 -1
- package/dist/router/wood_router.d.ts +1 -1
- package/dist/router/wood_router.js +9 -1
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/app_env.cjs +52 -0
- package/dist/runtime/app_env.cjs.map +1 -0
- package/dist/runtime/app_env.d.cts +10 -0
- package/dist/runtime/app_env.d.ts +10 -0
- package/dist/runtime/app_env.js +18 -0
- package/dist/runtime/app_env.js.map +1 -0
- package/dist/runtime/index.cjs +9 -2
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +5 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
- package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
- package/dist/runtime/runtime.cjs +75 -57
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.d.cts +8 -1
- package/dist/runtime/runtime.d.ts +8 -1
- package/dist/runtime/runtime.js +80 -58
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/testing/browser.cjs +540 -11
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +219 -3
- package/dist/testing/browser.d.ts +219 -3
- package/dist/testing/browser.js +540 -11
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -1
- package/dist/testing/create_controller_harness.d.ts +1 -1
- package/dist/testing/index.cjs +13 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +7 -2
- package/dist/testing/index.d.ts +7 -2
- package/dist/testing/index.js +14 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/trace-testing/index.d.cts +1 -1
- package/dist/trace-testing/index.d.ts +1 -1
- package/dist/trace-testing/trace_recorder.d.cts +1 -1
- package/dist/trace-testing/trace_recorder.d.ts +1 -1
- package/dist/trace-transport/index.cjs +32 -0
- package/dist/trace-transport/index.cjs.map +1 -0
- package/dist/trace-transport/index.d.cts +6 -0
- package/dist/trace-transport/index.d.ts +6 -0
- package/dist/trace-transport/index.js +7 -0
- package/dist/trace-transport/index.js.map +1 -0
- package/dist/trace_hub-Cob54qRx.d.cts +56 -0
- package/dist/trace_hub-Cob54qRx.d.ts +56 -0
- package/dist/tracing/index.cjs +14 -0
- package/dist/tracing/index.cjs.map +1 -1
- package/dist/tracing/index.d.cts +7 -1
- package/dist/tracing/index.d.ts +7 -1
- package/dist/tracing/index.js +12 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
- package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
- package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
- package/dist/tracing/renderer_trace_batch_host.js +56 -0
- package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.js +292 -0
- package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
- package/dist/tracing/trace_commit_writer.cjs +159 -0
- package/dist/tracing/trace_commit_writer.cjs.map +1 -0
- package/dist/tracing/trace_commit_writer.d.cts +97 -0
- package/dist/tracing/trace_commit_writer.d.ts +97 -0
- package/dist/tracing/trace_commit_writer.js +135 -0
- package/dist/tracing/trace_commit_writer.js.map +1 -0
- package/dist/tracing/trace_hub.cjs +58 -3
- package/dist/tracing/trace_hub.cjs.map +1 -1
- package/dist/tracing/trace_hub.d.cts +2 -46
- package/dist/tracing/trace_hub.d.ts +2 -46
- package/dist/tracing/trace_hub.js +56 -3
- package/dist/tracing/trace_hub.js.map +1 -1
- package/dist/tracing/trace_provider.cjs +42 -12
- package/dist/tracing/trace_provider.cjs.map +1 -1
- package/dist/tracing/trace_provider.d.cts +15 -1
- package/dist/tracing/trace_provider.d.ts +15 -1
- package/dist/tracing/trace_provider.js +43 -12
- package/dist/tracing/trace_provider.js.map +1 -1
- package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.js +1 -0
- package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.js +1 -0
- package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
- package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
- package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
- package/dist/tracing/wood_trace_batch_contract.js +205 -0
- package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
- package/dist/transport/build_transport_bridge.cjs +62 -0
- package/dist/transport/build_transport_bridge.cjs.map +1 -0
- package/dist/transport/build_transport_bridge.d.cts +29 -0
- package/dist/transport/build_transport_bridge.d.ts +29 -0
- package/dist/transport/build_transport_bridge.js +38 -0
- package/dist/transport/build_transport_bridge.js.map +1 -0
- package/dist/transport/in_process_router_transport.cjs +41 -0
- package/dist/transport/in_process_router_transport.cjs.map +1 -0
- package/dist/transport/in_process_router_transport.d.cts +31 -0
- package/dist/transport/in_process_router_transport.d.ts +31 -0
- package/dist/transport/in_process_router_transport.js +17 -0
- package/dist/transport/in_process_router_transport.js.map +1 -0
- package/dist/transport/index.cjs +42 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +8 -0
- package/dist/transport/index.d.ts +8 -0
- package/dist/transport/index.js +19 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/loopback_remote_transport.cjs +270 -0
- package/dist/transport/loopback_remote_transport.cjs.map +1 -0
- package/dist/transport/loopback_remote_transport.d.cts +49 -0
- package/dist/transport/loopback_remote_transport.d.ts +49 -0
- package/dist/transport/loopback_remote_transport.js +237 -0
- package/dist/transport/loopback_remote_transport.js.map +1 -0
- package/dist/transport/loopback_serialization.cjs +116 -0
- package/dist/transport/loopback_serialization.cjs.map +1 -0
- package/dist/transport/loopback_serialization.d.cts +28 -0
- package/dist/transport/loopback_serialization.d.ts +28 -0
- package/dist/transport/loopback_serialization.js +91 -0
- package/dist/transport/loopback_serialization.js.map +1 -0
- package/dist/transport/transport_contract_suite.cjs +110 -0
- package/dist/transport/transport_contract_suite.cjs.map +1 -0
- package/dist/transport/transport_contract_suite.d.cts +23 -0
- package/dist/transport/transport_contract_suite.d.ts +23 -0
- package/dist/transport/transport_contract_suite.js +85 -0
- package/dist/transport/transport_contract_suite.js.map +1 -0
- package/dist/transport/wood_rpc_transport.cjs +17 -0
- package/dist/transport/wood_rpc_transport.cjs.map +1 -0
- package/dist/transport/wood_rpc_transport.d.cts +44 -0
- package/dist/transport/wood_rpc_transport.d.ts +44 -0
- package/dist/transport/wood_rpc_transport.js +1 -0
- package/dist/transport/wood_rpc_transport.js.map +1 -0
- package/dist/types/config.cjs.map +1 -1
- package/dist/types/config.d.cts +12 -0
- package/dist/types/config.d.ts +12 -0
- package/dist/window/window_manager.cjs +3 -0
- package/dist/window/window_manager.cjs.map +1 -1
- package/dist/window/window_manager.js +3 -0
- package/dist/window/window_manager.js.map +1 -1
- package/docs/browser-testing.md +139 -1
- package/package.json +22 -3
- package/src/components/NavigationShell.browser.test.ts +236 -0
- package/src/components/NavigationShell.svelte +162 -49
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { InProcessRouterTransport } from "./in_process_router_transport.js";
|
|
2
|
+
import {
|
|
3
|
+
LoopbackRemoteTransport,
|
|
4
|
+
LoopbackRemoteTransportServer
|
|
5
|
+
} from "./loopback_remote_transport.js";
|
|
6
|
+
import {
|
|
7
|
+
assertLoopbackSerializable,
|
|
8
|
+
LoopbackSerializationError
|
|
9
|
+
} from "./loopback_serialization.js";
|
|
10
|
+
import { buildTransportBridge } from "./build_transport_bridge.js";
|
|
11
|
+
export {
|
|
12
|
+
InProcessRouterTransport,
|
|
13
|
+
LoopbackRemoteTransport,
|
|
14
|
+
LoopbackRemoteTransportServer,
|
|
15
|
+
LoopbackSerializationError,
|
|
16
|
+
assertLoopbackSerializable,
|
|
17
|
+
buildTransportBridge
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transport/index.ts"],"sourcesContent":["export type {\n WoodRpcTransport,\n WoodChannelContract,\n WoodChannelMapLike,\n WoodChannelInput,\n WoodChannelOutput,\n TypedCall,\n} from './wood_rpc_transport.js';\nexport { InProcessRouterTransport } from './in_process_router_transport.js';\nexport {\n LoopbackRemoteTransport,\n LoopbackRemoteTransportServer,\n} from './loopback_remote_transport.js';\nexport {\n assertLoopbackSerializable,\n LoopbackSerializationError,\n} from './loopback_serialization.js';\nexport { buildTransportBridge, type TransportBridgeResult } from './build_transport_bridge.js';\n"],"mappings":"AAQA,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAAwD;","names":[]}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var loopback_remote_transport_exports = {};
|
|
30
|
+
__export(loopback_remote_transport_exports, {
|
|
31
|
+
LoopbackRemoteTransport: () => LoopbackRemoteTransport,
|
|
32
|
+
LoopbackRemoteTransportServer: () => LoopbackRemoteTransportServer,
|
|
33
|
+
LoopbackSerializationError: () => import_loopback_serialization.LoopbackSerializationError
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(loopback_remote_transport_exports);
|
|
36
|
+
var import_node_http = __toESM(require("node:http"), 1);
|
|
37
|
+
var import_loopback_serialization = require("./loopback_serialization.js");
|
|
38
|
+
const LONG_POLL_TIMEOUT_MS = 25e3;
|
|
39
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
40
|
+
function readJsonBody(req) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
44
|
+
req.on("error", reject);
|
|
45
|
+
req.on("end", () => {
|
|
46
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
47
|
+
if (raw.length === 0) {
|
|
48
|
+
resolve(void 0);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
resolve(JSON.parse(raw));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
reject(error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function sendJson(res, status, body) {
|
|
60
|
+
const payload = JSON.stringify(body ?? null);
|
|
61
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
62
|
+
res.end(payload);
|
|
63
|
+
}
|
|
64
|
+
class LoopbackRemoteTransportServer {
|
|
65
|
+
constructor(transport) {
|
|
66
|
+
this.eventLog = [];
|
|
67
|
+
this.waiters = /* @__PURE__ */ new Set();
|
|
68
|
+
this.seq = 0;
|
|
69
|
+
this.listening = false;
|
|
70
|
+
this.transport = transport;
|
|
71
|
+
this.server = import_node_http.default.createServer((req, res) => {
|
|
72
|
+
this.handle(req, res).catch((error) => {
|
|
73
|
+
sendJson(res, 500, { message: error instanceof Error ? error.message : String(error) });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
get port() {
|
|
78
|
+
const address = this.server.address();
|
|
79
|
+
if (!address || typeof address === "string") {
|
|
80
|
+
throw new Error("Loopback server is not listening.");
|
|
81
|
+
}
|
|
82
|
+
return address.port;
|
|
83
|
+
}
|
|
84
|
+
get url() {
|
|
85
|
+
return `http://${LOOPBACK_HOST}:${this.port}`;
|
|
86
|
+
}
|
|
87
|
+
async listen() {
|
|
88
|
+
if (this.listening) return this.url;
|
|
89
|
+
await new Promise((resolve, reject) => {
|
|
90
|
+
this.server.once("error", reject);
|
|
91
|
+
this.server.listen(0, LOOPBACK_HOST, () => {
|
|
92
|
+
this.server.off("error", reject);
|
|
93
|
+
resolve();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
this.listening = true;
|
|
97
|
+
return this.url;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Push-event sink. Matches the `WindowEventService` sender signature: the
|
|
101
|
+
* channel arrives with the `__event:` prefix, which is stripped here.
|
|
102
|
+
*/
|
|
103
|
+
emit(channel, data) {
|
|
104
|
+
const stripped = channel.startsWith("__event:") ? channel.slice("__event:".length) : channel;
|
|
105
|
+
(0, import_loopback_serialization.assertLoopbackSerializable)(data, `event:${stripped}`);
|
|
106
|
+
const event = { seq: ++this.seq, channel: stripped, data };
|
|
107
|
+
this.eventLog.push(event);
|
|
108
|
+
for (const waiter of this.waiters) {
|
|
109
|
+
waiter([event]);
|
|
110
|
+
}
|
|
111
|
+
this.waiters.clear();
|
|
112
|
+
}
|
|
113
|
+
async close() {
|
|
114
|
+
for (const waiter of this.waiters) {
|
|
115
|
+
waiter([]);
|
|
116
|
+
}
|
|
117
|
+
this.waiters.clear();
|
|
118
|
+
if (!this.listening) return;
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
this.server.close((error) => error ? reject(error) : resolve());
|
|
121
|
+
});
|
|
122
|
+
this.listening = false;
|
|
123
|
+
}
|
|
124
|
+
async handle(req, res) {
|
|
125
|
+
const url = new URL(req.url ?? "/", this.url);
|
|
126
|
+
if (req.method === "POST" && url.pathname === "/invoke") {
|
|
127
|
+
await this.handleInvoke(req, res);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (req.method === "GET" && url.pathname === "/events") {
|
|
131
|
+
await this.handleEvents(url, res);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
sendJson(res, 404, { message: `No loopback route for ${req.method} ${url.pathname}` });
|
|
135
|
+
}
|
|
136
|
+
async handleInvoke(req, res) {
|
|
137
|
+
const body = await readJsonBody(req);
|
|
138
|
+
if (!body || typeof body.channel !== "string") {
|
|
139
|
+
sendJson(res, 400, { message: 'Loopback invoke requires a "channel".' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const result = await this.transport.invoke(body.channel, body.payload);
|
|
144
|
+
(0, import_loopback_serialization.assertLoopbackSerializable)(result, `result:${body.channel}`);
|
|
145
|
+
sendJson(res, 200, { ok: true, result: result ?? null });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const errorBody = {
|
|
148
|
+
message: error instanceof Error ? error.message : String(error),
|
|
149
|
+
name: error instanceof Error ? error.name : "Error"
|
|
150
|
+
};
|
|
151
|
+
sendJson(res, 200, { ok: false, error: errorBody });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
handleEvents(url, res) {
|
|
155
|
+
const cursor = Number.parseInt(url.searchParams.get("cursor") ?? "0", 10) || 0;
|
|
156
|
+
const pending = this.eventLog.filter((event) => event.seq > cursor);
|
|
157
|
+
if (pending.length > 0) {
|
|
158
|
+
sendJson(res, 200, { events: pending });
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
this.waiters.delete(waiter);
|
|
164
|
+
sendJson(res, 200, { events: [] });
|
|
165
|
+
resolve();
|
|
166
|
+
}, LONG_POLL_TIMEOUT_MS);
|
|
167
|
+
const waiter = (events) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
sendJson(res, 200, { events });
|
|
170
|
+
resolve();
|
|
171
|
+
};
|
|
172
|
+
this.waiters.add(waiter);
|
|
173
|
+
res.on("close", () => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
this.waiters.delete(waiter);
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
class LoopbackRemoteTransport {
|
|
182
|
+
constructor(baseUrl) {
|
|
183
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
184
|
+
this.cursor = 0;
|
|
185
|
+
this.polling = false;
|
|
186
|
+
this.closed = false;
|
|
187
|
+
this.pollController = null;
|
|
188
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
189
|
+
}
|
|
190
|
+
async invoke(channel, payload) {
|
|
191
|
+
(0, import_loopback_serialization.assertLoopbackSerializable)(payload, `payload:${channel}`);
|
|
192
|
+
const response = await fetch(`${this.baseUrl}/invoke`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: { "content-type": "application/json" },
|
|
195
|
+
body: JSON.stringify({ channel, payload })
|
|
196
|
+
});
|
|
197
|
+
const body = await response.json();
|
|
198
|
+
if (body.ok) {
|
|
199
|
+
return body.result;
|
|
200
|
+
}
|
|
201
|
+
const error = new Error(body.error.message);
|
|
202
|
+
error.name = body.error.name;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
subscribe(channel, listener) {
|
|
206
|
+
let set = this.listeners.get(channel);
|
|
207
|
+
if (!set) {
|
|
208
|
+
set = /* @__PURE__ */ new Set();
|
|
209
|
+
this.listeners.set(channel, set);
|
|
210
|
+
}
|
|
211
|
+
set.add(listener);
|
|
212
|
+
this.ensurePolling();
|
|
213
|
+
return () => {
|
|
214
|
+
set?.delete(listener);
|
|
215
|
+
if (set && set.size === 0) {
|
|
216
|
+
this.listeners.delete(channel);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** Stop the long-poll loop and release the in-flight request. Call in teardown. */
|
|
221
|
+
close() {
|
|
222
|
+
this.closed = true;
|
|
223
|
+
this.listeners.clear();
|
|
224
|
+
this.pollController?.abort();
|
|
225
|
+
}
|
|
226
|
+
ensurePolling() {
|
|
227
|
+
if (this.polling || this.closed) return;
|
|
228
|
+
this.polling = true;
|
|
229
|
+
void this.pollLoop();
|
|
230
|
+
}
|
|
231
|
+
async pollLoop() {
|
|
232
|
+
while (!this.closed && this.hasListeners()) {
|
|
233
|
+
try {
|
|
234
|
+
await this.pollOnce();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (this.closed || error?.name === "AbortError") break;
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.polling = false;
|
|
241
|
+
}
|
|
242
|
+
async pollOnce() {
|
|
243
|
+
this.pollController = new AbortController();
|
|
244
|
+
const response = await fetch(`${this.baseUrl}/events?cursor=${this.cursor}`, {
|
|
245
|
+
signal: this.pollController.signal
|
|
246
|
+
});
|
|
247
|
+
const body = await response.json();
|
|
248
|
+
for (const event of body.events) {
|
|
249
|
+
this.cursor = Math.max(this.cursor, event.seq);
|
|
250
|
+
this.dispatch(event.channel, event.data);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
dispatch(channel, data) {
|
|
254
|
+
const set = this.listeners.get(channel);
|
|
255
|
+
if (!set) return;
|
|
256
|
+
for (const listener of set) {
|
|
257
|
+
listener(data);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
hasListeners() {
|
|
261
|
+
return this.listeners.size > 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
265
|
+
0 && (module.exports = {
|
|
266
|
+
LoopbackRemoteTransport,
|
|
267
|
+
LoopbackRemoteTransportServer,
|
|
268
|
+
LoopbackSerializationError
|
|
269
|
+
});
|
|
270
|
+
//# sourceMappingURL=loopback_remote_transport.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transport/loopback_remote_transport.ts"],"sourcesContent":["import http from 'node:http';\nimport type { AddressInfo } from 'node:net';\nimport type { WoodRpcTransport } from './wood_rpc_transport.js';\nimport {\n assertLoopbackSerializable,\n LoopbackSerializationError,\n} from './loopback_serialization.js';\n\n/**\n * Loopback (127.0.0.1) HTTP transport — TEST ONLY.\n *\n * `invoke` is a plain HTTP request/response. Push events use HTTP long-polling\n * (no WebSocket dependency: `ws` is not a wood dependency and the loopback\n * transport is not a product feature, so long-polling over `node:http` is the\n * simplest faithful choice). JSON is the wire format, so both ends run the\n * strict `assertLoopbackSerializable` guard: anything JSON would coerce or drop\n * (Date, undefined, binary, class instances, non-finite numbers) is rejected\n * with a clear error rather than silently mangled. Electron structured-clone is\n * the ground-truth contract this guard defends.\n *\n * The server binds to `127.0.0.1` only and is never wired into production\n * startup (exported solely from the transport/testing entry points, which the\n * main runtime does not import).\n */\n\nconst LONG_POLL_TIMEOUT_MS = 25_000;\nconst LOOPBACK_HOST = '127.0.0.1';\n\ninterface PushEvent {\n seq: number;\n channel: string;\n data: unknown;\n}\n\ninterface InvokeErrorBody {\n message: string;\n name: string;\n}\n\nfunction readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('error', reject);\n req.on('end', () => {\n const raw = Buffer.concat(chunks).toString('utf-8');\n if (raw.length === 0) {\n resolve(undefined);\n return;\n }\n try {\n resolve(JSON.parse(raw));\n } catch (error) {\n reject(error);\n }\n });\n });\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown): void {\n const payload = JSON.stringify(body ?? null);\n res.writeHead(status, { 'content-type': 'application/json' });\n res.end(payload);\n}\n\n/**\n * The loopback server. Wraps a `WoodRpcTransport` for `invoke` and acts as the\n * push-event sink: wire `WindowEventService.configure((ch, data) => server.emit(ch, data))`.\n */\nexport class LoopbackRemoteTransportServer {\n private readonly transport: WoodRpcTransport;\n private readonly server: http.Server;\n private readonly eventLog: PushEvent[] = [];\n private readonly waiters = new Set<(events: PushEvent[]) => void>();\n private seq = 0;\n private listening = false;\n\n constructor(transport: WoodRpcTransport) {\n this.transport = transport;\n this.server = http.createServer((req, res) => {\n this.handle(req, res).catch((error) => {\n sendJson(res, 500, { message: error instanceof Error ? error.message : String(error) });\n });\n });\n }\n\n get port(): number {\n const address = this.server.address() as AddressInfo | null;\n if (!address || typeof address === 'string') {\n throw new Error('Loopback server is not listening.');\n }\n return address.port;\n }\n\n get url(): string {\n return `http://${LOOPBACK_HOST}:${this.port}`;\n }\n\n async listen(): Promise<string> {\n if (this.listening) return this.url;\n await new Promise<void>((resolve, reject) => {\n this.server.once('error', reject);\n this.server.listen(0, LOOPBACK_HOST, () => {\n this.server.off('error', reject);\n resolve();\n });\n });\n this.listening = true;\n return this.url;\n }\n\n /**\n * Push-event sink. Matches the `WindowEventService` sender signature: the\n * channel arrives with the `__event:` prefix, which is stripped here.\n */\n emit(channel: string, data: unknown): void {\n const stripped = channel.startsWith('__event:')\n ? channel.slice('__event:'.length)\n : channel;\n assertLoopbackSerializable(data, `event:${stripped}`);\n const event: PushEvent = { seq: ++this.seq, channel: stripped, data };\n this.eventLog.push(event);\n for (const waiter of this.waiters) {\n waiter([event]);\n }\n this.waiters.clear();\n }\n\n async close(): Promise<void> {\n for (const waiter of this.waiters) {\n waiter([]);\n }\n this.waiters.clear();\n if (!this.listening) return;\n await new Promise<void>((resolve, reject) => {\n this.server.close((error) => (error ? reject(error) : resolve()));\n });\n this.listening = false;\n }\n\n private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const url = new URL(req.url ?? '/', this.url);\n if (req.method === 'POST' && url.pathname === '/invoke') {\n await this.handleInvoke(req, res);\n return;\n }\n if (req.method === 'GET' && url.pathname === '/events') {\n await this.handleEvents(url, res);\n return;\n }\n sendJson(res, 404, { message: `No loopback route for ${req.method} ${url.pathname}` });\n }\n\n private async handleInvoke(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const body = (await readJsonBody(req)) as { channel?: string; payload?: unknown } | undefined;\n if (!body || typeof body.channel !== 'string') {\n sendJson(res, 400, { message: 'Loopback invoke requires a \"channel\".' });\n return;\n }\n\n try {\n const result = await this.transport.invoke(body.channel, body.payload);\n assertLoopbackSerializable(result, `result:${body.channel}`);\n sendJson(res, 200, { ok: true, result: result ?? null });\n } catch (error) {\n const errorBody: InvokeErrorBody = {\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : 'Error',\n };\n sendJson(res, 200, { ok: false, error: errorBody });\n }\n }\n\n private handleEvents(url: URL, res: http.ServerResponse): Promise<void> {\n const cursor = Number.parseInt(url.searchParams.get('cursor') ?? '0', 10) || 0;\n const pending = this.eventLog.filter((event) => event.seq > cursor);\n if (pending.length > 0) {\n sendJson(res, 200, { events: pending });\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.waiters.delete(waiter);\n sendJson(res, 200, { events: [] });\n resolve();\n }, LONG_POLL_TIMEOUT_MS);\n\n const waiter = (events: PushEvent[]): void => {\n clearTimeout(timer);\n sendJson(res, 200, { events });\n resolve();\n };\n\n this.waiters.add(waiter);\n res.on('close', () => {\n clearTimeout(timer);\n this.waiters.delete(waiter);\n resolve();\n });\n });\n }\n}\n\n/** Client-side loopback transport. Talks to a `LoopbackRemoteTransportServer`. */\nexport class LoopbackRemoteTransport implements WoodRpcTransport {\n private readonly baseUrl: string;\n private readonly listeners = new Map<string, Set<(payload: unknown) => void>>();\n private cursor = 0;\n private polling = false;\n private closed = false;\n private pollController: AbortController | null = null;\n\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '');\n }\n\n async invoke(channel: string, payload?: unknown): Promise<unknown> {\n assertLoopbackSerializable(payload, `payload:${channel}`);\n const response = await fetch(`${this.baseUrl}/invoke`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ channel, payload }),\n });\n const body = (await response.json()) as\n | { ok: true; result: unknown }\n | { ok: false; error: InvokeErrorBody };\n\n if (body.ok) {\n return body.result;\n }\n const error = new Error(body.error.message);\n error.name = body.error.name;\n throw error;\n }\n\n subscribe(channel: string, listener: (payload: unknown) => void): () => void {\n let set = this.listeners.get(channel);\n if (!set) {\n set = new Set();\n this.listeners.set(channel, set);\n }\n set.add(listener);\n this.ensurePolling();\n\n return () => {\n set?.delete(listener);\n if (set && set.size === 0) {\n this.listeners.delete(channel);\n }\n };\n }\n\n /** Stop the long-poll loop and release the in-flight request. Call in teardown. */\n close(): void {\n this.closed = true;\n this.listeners.clear();\n this.pollController?.abort();\n }\n\n private ensurePolling(): void {\n if (this.polling || this.closed) return;\n this.polling = true;\n void this.pollLoop();\n }\n\n private async pollLoop(): Promise<void> {\n while (!this.closed && this.hasListeners()) {\n try {\n await this.pollOnce();\n } catch (error) {\n if (this.closed || (error as Error)?.name === 'AbortError') break;\n // Transient loopback error: yield a tick and retry.\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n this.polling = false;\n }\n\n private async pollOnce(): Promise<void> {\n this.pollController = new AbortController();\n const response = await fetch(`${this.baseUrl}/events?cursor=${this.cursor}`, {\n signal: this.pollController.signal,\n });\n const body = (await response.json()) as { events: PushEvent[] };\n for (const event of body.events) {\n this.cursor = Math.max(this.cursor, event.seq);\n this.dispatch(event.channel, event.data);\n }\n }\n\n private dispatch(channel: string, data: unknown): void {\n const set = this.listeners.get(channel);\n if (!set) return;\n for (const listener of set) {\n listener(data);\n }\n }\n\n private hasListeners(): boolean {\n return this.listeners.size > 0;\n }\n}\n\nexport { LoopbackSerializationError };\n"],"mappings":"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","names":["http"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { WoodRpcTransport } from './wood_rpc_transport.cjs';
|
|
2
|
+
export { LoopbackSerializationError } from './loopback_serialization.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The loopback server. Wraps a `WoodRpcTransport` for `invoke` and acts as the
|
|
6
|
+
* push-event sink: wire `WindowEventService.configure((ch, data) => server.emit(ch, data))`.
|
|
7
|
+
*/
|
|
8
|
+
declare class LoopbackRemoteTransportServer {
|
|
9
|
+
private readonly transport;
|
|
10
|
+
private readonly server;
|
|
11
|
+
private readonly eventLog;
|
|
12
|
+
private readonly waiters;
|
|
13
|
+
private seq;
|
|
14
|
+
private listening;
|
|
15
|
+
constructor(transport: WoodRpcTransport);
|
|
16
|
+
get port(): number;
|
|
17
|
+
get url(): string;
|
|
18
|
+
listen(): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Push-event sink. Matches the `WindowEventService` sender signature: the
|
|
21
|
+
* channel arrives with the `__event:` prefix, which is stripped here.
|
|
22
|
+
*/
|
|
23
|
+
emit(channel: string, data: unknown): void;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
private handle;
|
|
26
|
+
private handleInvoke;
|
|
27
|
+
private handleEvents;
|
|
28
|
+
}
|
|
29
|
+
/** Client-side loopback transport. Talks to a `LoopbackRemoteTransportServer`. */
|
|
30
|
+
declare class LoopbackRemoteTransport implements WoodRpcTransport {
|
|
31
|
+
private readonly baseUrl;
|
|
32
|
+
private readonly listeners;
|
|
33
|
+
private cursor;
|
|
34
|
+
private polling;
|
|
35
|
+
private closed;
|
|
36
|
+
private pollController;
|
|
37
|
+
constructor(baseUrl: string);
|
|
38
|
+
invoke(channel: string, payload?: unknown): Promise<unknown>;
|
|
39
|
+
subscribe(channel: string, listener: (payload: unknown) => void): () => void;
|
|
40
|
+
/** Stop the long-poll loop and release the in-flight request. Call in teardown. */
|
|
41
|
+
close(): void;
|
|
42
|
+
private ensurePolling;
|
|
43
|
+
private pollLoop;
|
|
44
|
+
private pollOnce;
|
|
45
|
+
private dispatch;
|
|
46
|
+
private hasListeners;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { LoopbackRemoteTransport, LoopbackRemoteTransportServer };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { WoodRpcTransport } from './wood_rpc_transport.js';
|
|
2
|
+
export { LoopbackSerializationError } from './loopback_serialization.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The loopback server. Wraps a `WoodRpcTransport` for `invoke` and acts as the
|
|
6
|
+
* push-event sink: wire `WindowEventService.configure((ch, data) => server.emit(ch, data))`.
|
|
7
|
+
*/
|
|
8
|
+
declare class LoopbackRemoteTransportServer {
|
|
9
|
+
private readonly transport;
|
|
10
|
+
private readonly server;
|
|
11
|
+
private readonly eventLog;
|
|
12
|
+
private readonly waiters;
|
|
13
|
+
private seq;
|
|
14
|
+
private listening;
|
|
15
|
+
constructor(transport: WoodRpcTransport);
|
|
16
|
+
get port(): number;
|
|
17
|
+
get url(): string;
|
|
18
|
+
listen(): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Push-event sink. Matches the `WindowEventService` sender signature: the
|
|
21
|
+
* channel arrives with the `__event:` prefix, which is stripped here.
|
|
22
|
+
*/
|
|
23
|
+
emit(channel: string, data: unknown): void;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
private handle;
|
|
26
|
+
private handleInvoke;
|
|
27
|
+
private handleEvents;
|
|
28
|
+
}
|
|
29
|
+
/** Client-side loopback transport. Talks to a `LoopbackRemoteTransportServer`. */
|
|
30
|
+
declare class LoopbackRemoteTransport implements WoodRpcTransport {
|
|
31
|
+
private readonly baseUrl;
|
|
32
|
+
private readonly listeners;
|
|
33
|
+
private cursor;
|
|
34
|
+
private polling;
|
|
35
|
+
private closed;
|
|
36
|
+
private pollController;
|
|
37
|
+
constructor(baseUrl: string);
|
|
38
|
+
invoke(channel: string, payload?: unknown): Promise<unknown>;
|
|
39
|
+
subscribe(channel: string, listener: (payload: unknown) => void): () => void;
|
|
40
|
+
/** Stop the long-poll loop and release the in-flight request. Call in teardown. */
|
|
41
|
+
close(): void;
|
|
42
|
+
private ensurePolling;
|
|
43
|
+
private pollLoop;
|
|
44
|
+
private pollOnce;
|
|
45
|
+
private dispatch;
|
|
46
|
+
private hasListeners;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { LoopbackRemoteTransport, LoopbackRemoteTransportServer };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import {
|
|
3
|
+
assertLoopbackSerializable,
|
|
4
|
+
LoopbackSerializationError
|
|
5
|
+
} from "./loopback_serialization.js";
|
|
6
|
+
const LONG_POLL_TIMEOUT_MS = 25e3;
|
|
7
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
8
|
+
function readJsonBody(req) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
12
|
+
req.on("error", reject);
|
|
13
|
+
req.on("end", () => {
|
|
14
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
15
|
+
if (raw.length === 0) {
|
|
16
|
+
resolve(void 0);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
resolve(JSON.parse(raw));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
reject(error);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function sendJson(res, status, body) {
|
|
28
|
+
const payload = JSON.stringify(body ?? null);
|
|
29
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
30
|
+
res.end(payload);
|
|
31
|
+
}
|
|
32
|
+
class LoopbackRemoteTransportServer {
|
|
33
|
+
constructor(transport) {
|
|
34
|
+
this.eventLog = [];
|
|
35
|
+
this.waiters = /* @__PURE__ */ new Set();
|
|
36
|
+
this.seq = 0;
|
|
37
|
+
this.listening = false;
|
|
38
|
+
this.transport = transport;
|
|
39
|
+
this.server = http.createServer((req, res) => {
|
|
40
|
+
this.handle(req, res).catch((error) => {
|
|
41
|
+
sendJson(res, 500, { message: error instanceof Error ? error.message : String(error) });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
get port() {
|
|
46
|
+
const address = this.server.address();
|
|
47
|
+
if (!address || typeof address === "string") {
|
|
48
|
+
throw new Error("Loopback server is not listening.");
|
|
49
|
+
}
|
|
50
|
+
return address.port;
|
|
51
|
+
}
|
|
52
|
+
get url() {
|
|
53
|
+
return `http://${LOOPBACK_HOST}:${this.port}`;
|
|
54
|
+
}
|
|
55
|
+
async listen() {
|
|
56
|
+
if (this.listening) return this.url;
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
this.server.once("error", reject);
|
|
59
|
+
this.server.listen(0, LOOPBACK_HOST, () => {
|
|
60
|
+
this.server.off("error", reject);
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
this.listening = true;
|
|
65
|
+
return this.url;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Push-event sink. Matches the `WindowEventService` sender signature: the
|
|
69
|
+
* channel arrives with the `__event:` prefix, which is stripped here.
|
|
70
|
+
*/
|
|
71
|
+
emit(channel, data) {
|
|
72
|
+
const stripped = channel.startsWith("__event:") ? channel.slice("__event:".length) : channel;
|
|
73
|
+
assertLoopbackSerializable(data, `event:${stripped}`);
|
|
74
|
+
const event = { seq: ++this.seq, channel: stripped, data };
|
|
75
|
+
this.eventLog.push(event);
|
|
76
|
+
for (const waiter of this.waiters) {
|
|
77
|
+
waiter([event]);
|
|
78
|
+
}
|
|
79
|
+
this.waiters.clear();
|
|
80
|
+
}
|
|
81
|
+
async close() {
|
|
82
|
+
for (const waiter of this.waiters) {
|
|
83
|
+
waiter([]);
|
|
84
|
+
}
|
|
85
|
+
this.waiters.clear();
|
|
86
|
+
if (!this.listening) return;
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
this.server.close((error) => error ? reject(error) : resolve());
|
|
89
|
+
});
|
|
90
|
+
this.listening = false;
|
|
91
|
+
}
|
|
92
|
+
async handle(req, res) {
|
|
93
|
+
const url = new URL(req.url ?? "/", this.url);
|
|
94
|
+
if (req.method === "POST" && url.pathname === "/invoke") {
|
|
95
|
+
await this.handleInvoke(req, res);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (req.method === "GET" && url.pathname === "/events") {
|
|
99
|
+
await this.handleEvents(url, res);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
sendJson(res, 404, { message: `No loopback route for ${req.method} ${url.pathname}` });
|
|
103
|
+
}
|
|
104
|
+
async handleInvoke(req, res) {
|
|
105
|
+
const body = await readJsonBody(req);
|
|
106
|
+
if (!body || typeof body.channel !== "string") {
|
|
107
|
+
sendJson(res, 400, { message: 'Loopback invoke requires a "channel".' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.transport.invoke(body.channel, body.payload);
|
|
112
|
+
assertLoopbackSerializable(result, `result:${body.channel}`);
|
|
113
|
+
sendJson(res, 200, { ok: true, result: result ?? null });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const errorBody = {
|
|
116
|
+
message: error instanceof Error ? error.message : String(error),
|
|
117
|
+
name: error instanceof Error ? error.name : "Error"
|
|
118
|
+
};
|
|
119
|
+
sendJson(res, 200, { ok: false, error: errorBody });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
handleEvents(url, res) {
|
|
123
|
+
const cursor = Number.parseInt(url.searchParams.get("cursor") ?? "0", 10) || 0;
|
|
124
|
+
const pending = this.eventLog.filter((event) => event.seq > cursor);
|
|
125
|
+
if (pending.length > 0) {
|
|
126
|
+
sendJson(res, 200, { events: pending });
|
|
127
|
+
return Promise.resolve();
|
|
128
|
+
}
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
this.waiters.delete(waiter);
|
|
132
|
+
sendJson(res, 200, { events: [] });
|
|
133
|
+
resolve();
|
|
134
|
+
}, LONG_POLL_TIMEOUT_MS);
|
|
135
|
+
const waiter = (events) => {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
sendJson(res, 200, { events });
|
|
138
|
+
resolve();
|
|
139
|
+
};
|
|
140
|
+
this.waiters.add(waiter);
|
|
141
|
+
res.on("close", () => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
this.waiters.delete(waiter);
|
|
144
|
+
resolve();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
class LoopbackRemoteTransport {
|
|
150
|
+
constructor(baseUrl) {
|
|
151
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
152
|
+
this.cursor = 0;
|
|
153
|
+
this.polling = false;
|
|
154
|
+
this.closed = false;
|
|
155
|
+
this.pollController = null;
|
|
156
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
157
|
+
}
|
|
158
|
+
async invoke(channel, payload) {
|
|
159
|
+
assertLoopbackSerializable(payload, `payload:${channel}`);
|
|
160
|
+
const response = await fetch(`${this.baseUrl}/invoke`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "content-type": "application/json" },
|
|
163
|
+
body: JSON.stringify({ channel, payload })
|
|
164
|
+
});
|
|
165
|
+
const body = await response.json();
|
|
166
|
+
if (body.ok) {
|
|
167
|
+
return body.result;
|
|
168
|
+
}
|
|
169
|
+
const error = new Error(body.error.message);
|
|
170
|
+
error.name = body.error.name;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
subscribe(channel, listener) {
|
|
174
|
+
let set = this.listeners.get(channel);
|
|
175
|
+
if (!set) {
|
|
176
|
+
set = /* @__PURE__ */ new Set();
|
|
177
|
+
this.listeners.set(channel, set);
|
|
178
|
+
}
|
|
179
|
+
set.add(listener);
|
|
180
|
+
this.ensurePolling();
|
|
181
|
+
return () => {
|
|
182
|
+
set?.delete(listener);
|
|
183
|
+
if (set && set.size === 0) {
|
|
184
|
+
this.listeners.delete(channel);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Stop the long-poll loop and release the in-flight request. Call in teardown. */
|
|
189
|
+
close() {
|
|
190
|
+
this.closed = true;
|
|
191
|
+
this.listeners.clear();
|
|
192
|
+
this.pollController?.abort();
|
|
193
|
+
}
|
|
194
|
+
ensurePolling() {
|
|
195
|
+
if (this.polling || this.closed) return;
|
|
196
|
+
this.polling = true;
|
|
197
|
+
void this.pollLoop();
|
|
198
|
+
}
|
|
199
|
+
async pollLoop() {
|
|
200
|
+
while (!this.closed && this.hasListeners()) {
|
|
201
|
+
try {
|
|
202
|
+
await this.pollOnce();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (this.closed || error?.name === "AbortError") break;
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
this.polling = false;
|
|
209
|
+
}
|
|
210
|
+
async pollOnce() {
|
|
211
|
+
this.pollController = new AbortController();
|
|
212
|
+
const response = await fetch(`${this.baseUrl}/events?cursor=${this.cursor}`, {
|
|
213
|
+
signal: this.pollController.signal
|
|
214
|
+
});
|
|
215
|
+
const body = await response.json();
|
|
216
|
+
for (const event of body.events) {
|
|
217
|
+
this.cursor = Math.max(this.cursor, event.seq);
|
|
218
|
+
this.dispatch(event.channel, event.data);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
dispatch(channel, data) {
|
|
222
|
+
const set = this.listeners.get(channel);
|
|
223
|
+
if (!set) return;
|
|
224
|
+
for (const listener of set) {
|
|
225
|
+
listener(data);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
hasListeners() {
|
|
229
|
+
return this.listeners.size > 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export {
|
|
233
|
+
LoopbackRemoteTransport,
|
|
234
|
+
LoopbackRemoteTransportServer,
|
|
235
|
+
LoopbackSerializationError
|
|
236
|
+
};
|
|
237
|
+
//# sourceMappingURL=loopback_remote_transport.js.map
|