@almadar/shell-runtime 0.0.0-stage → 1.0.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/chunk-4SEK5ER2.js +8 -0
- package/dist/chunk-57SBCUSM.js +62 -0
- package/dist/chunk-625EP7W2.js +108 -0
- package/dist/chunk-64IH2SMY.js +22 -0
- package/dist/chunk-7SDV3QWR.js +67 -0
- package/dist/chunk-7WB6VWRK.js +798 -0
- package/dist/chunk-A2JJT4GA.js +56 -0
- package/dist/chunk-BAS26HBN.js +10 -0
- package/dist/chunk-BOVU4TCQ.js +65 -0
- package/dist/chunk-BY5G4225.js +6 -0
- package/dist/chunk-DFDNHVDW.js +284 -0
- package/dist/chunk-ELBLT2HZ.js +1 -0
- package/dist/chunk-EZZYLTWY.js +1 -0
- package/dist/chunk-FWA5K7Q2.js +48 -0
- package/dist/chunk-HC6ERSAH.js +55 -0
- package/dist/chunk-HLOUDLPI.js +48 -0
- package/dist/chunk-I3TKINSO.js +398 -0
- package/dist/chunk-IOPUS6Q5.js +15 -0
- package/dist/chunk-LKWYCYX5.js +22 -0
- package/dist/chunk-LYFIQPXX.js +1 -0
- package/dist/chunk-LZIOTBFX.js +1 -0
- package/dist/chunk-MU335ZA4.js +63 -0
- package/dist/chunk-MZKIL5IN.js +74 -0
- package/dist/chunk-N7HYNKK6.js +83 -0
- package/dist/chunk-NTFQ36UN.js +52 -0
- package/dist/chunk-NU6LVKGS.js +15 -0
- package/dist/chunk-O57GKTYU.js +30 -0
- package/dist/chunk-P2N6K2IV.js +20 -0
- package/dist/chunk-P53CKOME.js +1 -0
- package/dist/chunk-QCJWPXLH.js +40 -0
- package/dist/chunk-SKLMWHQD.js +109 -0
- package/dist/chunk-SOQDEPME.js +131 -0
- package/dist/chunk-UBN5JJKZ.js +26 -0
- package/dist/chunk-UD2CXYSJ.js +244 -0
- package/dist/chunk-UP6GGGAN.js +34 -0
- package/dist/chunk-V4EMDPCO.js +33 -0
- package/dist/chunk-VNGXRYFF.js +9 -0
- package/dist/chunk-WGCWJOT2.js +21 -0
- package/dist/chunk-X6W44WHO.js +26 -0
- package/dist/chunk-XXWNMJI6.js +15 -0
- package/dist/chunk-ZUSM4VW4.js +197 -0
- package/dist/client/EventTransport.d.ts +147 -0
- package/dist/client/EventTransport.js +1 -0
- package/dist/client/awaiting.d.ts +28 -0
- package/dist/client/awaiting.js +1 -0
- package/dist/client/channel-transport.d.ts +69 -0
- package/dist/client/channel-transport.js +1 -0
- package/dist/client/circuit-store.d.ts +87 -0
- package/dist/client/circuit-store.js +3 -0
- package/dist/client/client-kernel.d.ts +134 -0
- package/dist/client/client-kernel.js +1 -0
- package/dist/client/declared-input-dispatch.d.ts +22 -0
- package/dist/client/declared-input-dispatch.js +1 -0
- package/dist/client/inbound-dispatch.d.ts +24 -0
- package/dist/client/inbound-dispatch.js +1 -0
- package/dist/client/local-fold.d.ts +33 -0
- package/dist/client/local-fold.js +1 -0
- package/dist/client/render-sigils.d.ts +24 -0
- package/dist/client/render-sigils.js +1 -0
- package/dist/client/state-book.d.ts +30 -0
- package/dist/client/state-book.js +1 -0
- package/dist/client/trait-mount-error.d.ts +9 -0
- package/dist/client/trait-mount-error.js +1 -0
- package/dist/client/trait-types.d.ts +67 -0
- package/dist/client/trait-types.js +1 -0
- package/dist/credentials/env-vars.d.ts +18 -0
- package/dist/credentials/env-vars.js +1 -0
- package/dist/credentials/scope.d.ts +51 -0
- package/dist/credentials/scope.js +1 -0
- package/dist/credentials/vault.d.ts +142 -0
- package/dist/credentials/vault.js +1 -0
- package/dist/effects/in-flight-calls.d.ts +36 -0
- package/dist/effects/in-flight-calls.js +1 -0
- package/dist/effects/on-device-model.d.ts +22 -0
- package/dist/effects/on-device-model.js +1 -0
- package/dist/events/EventBus.d.ts +87 -0
- package/dist/events/EventBus.js +1 -0
- package/dist/events/emit-stamp.d.ts +47 -0
- package/dist/events/emit-stamp.js +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +41 -0
- package/dist/kernel/decode.d.ts +13 -0
- package/dist/kernel/decode.js +1 -0
- package/dist/kernel/engine.d.ts +195 -0
- package/dist/kernel/engine.js +9 -0
- package/dist/kernel/frame-applier.d.ts +44 -0
- package/dist/kernel/frame-applier.js +1 -0
- package/dist/kernel/host.d.ts +206 -0
- package/dist/kernel/host.js +6 -0
- package/dist/kernel/lane.d.ts +7 -0
- package/dist/kernel/lane.js +1 -0
- package/dist/kernel/os-fetch-port.d.ts +7 -0
- package/dist/kernel/os-fetch-port.js +3 -0
- package/dist/kernel/os-port.d.ts +155 -0
- package/dist/kernel/os-port.js +1 -0
- package/dist/kernel/program-effects.d.ts +48 -0
- package/dist/kernel/program-effects.js +1 -0
- package/dist/kernel/secret-ports.d.ts +139 -0
- package/dist/kernel/secret-ports.js +2 -0
- package/dist/kernel/wire.d.ts +104 -0
- package/dist/kernel/wire.js +1 -0
- package/dist/loader/fetch-loader.d.ts +26 -0
- package/dist/loader/fetch-loader.js +1 -0
- package/dist/loader/lazy-page.d.ts +12 -0
- package/dist/loader/lazy-page.js +1 -0
- package/dist/loader/schema-loader.d.ts +144 -0
- package/dist/loader/schema-loader.js +1 -0
- package/dist/persistence/native-memory.d.ts +28 -0
- package/dist/persistence/native-memory.js +1 -0
- package/dist/runtime/entities/declaredCollections.d.ts +6 -0
- package/dist/runtime/entities/declaredCollections.js +1 -0
- package/dist/runtime/evaluation/local-store-transport.d.ts +59 -0
- package/dist/runtime/evaluation/local-store-transport.js +14 -0
- package/dist/runtime/evaluation/residence-transport.d.ts +15 -0
- package/dist/runtime/evaluation/residence-transport.js +1 -0
- package/dist/runtime/evaluation/routed-persistence.d.ts +9 -0
- package/dist/runtime/evaluation/routed-persistence.js +1 -0
- package/dist/runtime/events/identity/routing.d.ts +33 -0
- package/dist/runtime/events/identity/routing.js +1 -0
- package/dist/runtime/types.d.ts +374 -0
- package/dist/runtime/types.js +1 -0
- package/dist/service/default-call-service.d.ts +23 -0
- package/dist/service/default-call-service.js +2 -0
- package/dist/service/service-provider.d.ts +37 -0
- package/dist/service/service-provider.js +1 -0
- package/package.json +49 -3
- package/README.md +0 -3
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isFrameWire } from '@almadar/core/kernel-wire';
|
|
2
|
+
import { createLogger } from '@almadar/logger';
|
|
3
|
+
|
|
4
|
+
// src/kernel/frame-applier.ts
|
|
5
|
+
var log = createLogger("almadar:shell-web:frame");
|
|
6
|
+
function decodeFrame(json) {
|
|
7
|
+
if (json === void 0) return void 0;
|
|
8
|
+
const value = JSON.parse(json);
|
|
9
|
+
if (!isFrameWire(value)) throw new Error("kernel wire: frame is not a FrameWire");
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
var NOTHING = { fired: {}, firedCount: 0, folded: 0, browserLegs: 0, droppedServerEffects: 0, outcomes: [], errors: [] };
|
|
13
|
+
var settle = (posted, failure, trait) => posted.then(
|
|
14
|
+
(effects) => effects,
|
|
15
|
+
(err) => {
|
|
16
|
+
log.warn(failure, { trait, error: String(err) });
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
async function applyFrame(json, kernel, post) {
|
|
21
|
+
const frame = decodeFrame(json);
|
|
22
|
+
if (frame === void 0) return NOTHING;
|
|
23
|
+
for (const message of frame.errors) log.warn("frame-tick-failed", { error: message });
|
|
24
|
+
const last = frame.dispatches.length - 1;
|
|
25
|
+
await kernel.foldLocalResponse(frame.response, false, last < 0);
|
|
26
|
+
for (const [i, dispatch] of frame.dispatches.entries()) await kernel.foldLocalResponse(dispatch.response, true, i === last);
|
|
27
|
+
const outcomes = [{ response: frame.response, mode: "hybridClientOnly" }];
|
|
28
|
+
if (post !== void 0) {
|
|
29
|
+
for (const leg of frame.browserLegs) {
|
|
30
|
+
const tickSettled = settle(post.sendBrowserLeg(leg, frame.response), "tick-browser-leg-failed", leg.leg.targetTrait ?? "");
|
|
31
|
+
outcomes.push({ response: { success: true, transitioned: false, states: {}, emittedEvents: [] }, mode: "hybridClientOnly", tickSettled });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const dispatch of frame.dispatches) {
|
|
35
|
+
if (post === void 0 || dispatch.serverLegs.length === 0 && dispatch.statefulLeg === null) {
|
|
36
|
+
outcomes.push({ response: dispatch.response, mode: dispatch.mode });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
outcomes.push({ response: dispatch.response, mode: dispatch.mode, tickSettled: settle(post.postDispatch(dispatch), "tick-post-failed", dispatch.trait) });
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
fired: frame.fired,
|
|
43
|
+
firedCount: Object.values(frame.fired).reduce((n, ticks) => n + ticks.length, 0),
|
|
44
|
+
folded: frame.dispatches.length + 1,
|
|
45
|
+
browserLegs: frame.browserLegs.length,
|
|
46
|
+
droppedServerEffects: frame.droppedServerEffects,
|
|
47
|
+
outcomes,
|
|
48
|
+
errors: frame.errors
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { applyFrame, decodeFrame };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/kernel/lane.ts
|
|
2
|
+
var LaneHeldByParkedDispatch = class extends Error {
|
|
3
|
+
ticket;
|
|
4
|
+
event;
|
|
5
|
+
constructor(ticket, event) {
|
|
6
|
+
super(
|
|
7
|
+
`dispatch #${ticket} (${event}) is parked holding the dispatch lane on this thread; resume or drop it before starting another dispatch`
|
|
8
|
+
);
|
|
9
|
+
this.name = "LaneHeldByParkedDispatch";
|
|
10
|
+
this.ticket = ticket;
|
|
11
|
+
this.event = event;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export { LaneHeldByParkedDispatch };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/runtime/evaluation/routed-persistence.ts
|
|
2
|
+
function pushedDown(method, name, entityType) {
|
|
3
|
+
if (method === void 0) throw new Error(`${name} is not supported by the store that holds ${entityType}`);
|
|
4
|
+
return method;
|
|
5
|
+
}
|
|
6
|
+
function routeByEntity(storedEntities, stored, other) {
|
|
7
|
+
const of = (entityType) => storedEntities.has(entityType) ? stored : other;
|
|
8
|
+
return {
|
|
9
|
+
create: (entityType, data) => of(entityType).create(entityType, data),
|
|
10
|
+
update: (entityType, id, data) => of(entityType).update(entityType, id, data),
|
|
11
|
+
delete: (entityType, id) => of(entityType).delete(entityType, id),
|
|
12
|
+
getById: (entityType, id) => of(entityType).getById(entityType, id),
|
|
13
|
+
list: (entityType) => of(entityType).list(entityType),
|
|
14
|
+
bindCollection: (entityType, collection) => of(entityType).bindCollection?.(entityType, collection),
|
|
15
|
+
queryPage: (entityType, query) => pushedDown(of(entityType).queryPage, "queryPage", entityType).call(of(entityType), entityType, query),
|
|
16
|
+
queryIds: (entityType, query) => pushedDown(of(entityType).queryIds, "queryIds", entityType).call(of(entityType), entityType, query),
|
|
17
|
+
queryCapabilities: (entityType) => pushedDown(of(entityType).queryCapabilities, "queryCapabilities", entityType).call(of(entityType), entityType),
|
|
18
|
+
writeCapabilities: (entityType) => pushedDown(of(entityType).writeCapabilities, "writeCapabilities", entityType).call(of(entityType), entityType),
|
|
19
|
+
writeWhere: (op) => pushedDown(of(op.entityType).writeWhere, "writeWhere", op.entityType).call(of(op.entityType), op),
|
|
20
|
+
writeBatch: async (batch) => {
|
|
21
|
+
const homes = new Set(batch.ops.map((op) => of(op.entityType)));
|
|
22
|
+
if (homes.size > 1) throw new Error("A batch spans the browser store and the in-memory rows; a batch runs in one store");
|
|
23
|
+
const [home] = homes;
|
|
24
|
+
if (home === void 0) return [];
|
|
25
|
+
return pushedDown(home.writeBatch, "writeBatch", batch.ops[0]?.entityType ?? "").call(home, batch);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { routeByEntity };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/client/inbound-dispatch.ts
|
|
2
|
+
async function dispatchInbound(contract, name, delivery, send, credential) {
|
|
3
|
+
const found = contract.findInbound(name);
|
|
4
|
+
if (!("route" in found)) return found.answer;
|
|
5
|
+
const { route } = found;
|
|
6
|
+
try {
|
|
7
|
+
const held = route.verifies !== void 0 ? await credential(route.verifies.credential) : void 0;
|
|
8
|
+
if (route.verifies !== void 0 && (held === void 0 || held === null || held === "")) return contract.credentialUnset(route.verifies.credential);
|
|
9
|
+
const response = await send(route.orbital, {
|
|
10
|
+
event: route.event,
|
|
11
|
+
payload: contract.inboundPayload(route, delivery, held),
|
|
12
|
+
targetTrait: route.trait
|
|
13
|
+
});
|
|
14
|
+
return contract.inboundAnswer(route, response.emittedEvents, response.error);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return contract.dispatchFailed(error instanceof Error ? error.message : String(error));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { dispatchInbound };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/client/local-fold.ts
|
|
2
|
+
function paintStates(store, response) {
|
|
3
|
+
for (const [trait, state] of Object.entries(response.states)) {
|
|
4
|
+
if (store.states.getState(trait)?.currentState !== state) store.states.seedState(trait, state);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function paintFrames(store, delta, framesSince) {
|
|
8
|
+
if (delta === void 0) return;
|
|
9
|
+
if (store.applyFrames(delta) === "gap") store.applyFrames(framesSince(store.frameVersion));
|
|
10
|
+
}
|
|
11
|
+
function paintResponse(store, framesSince, response) {
|
|
12
|
+
paintStates(store, response);
|
|
13
|
+
paintFrames(store, response.frames, framesSince);
|
|
14
|
+
}
|
|
15
|
+
function traceResponse(observer, response) {
|
|
16
|
+
if (observer === void 0) return;
|
|
17
|
+
for (const hop of response.transitions ?? []) {
|
|
18
|
+
observer.onTransition({
|
|
19
|
+
traitName: hop.trait,
|
|
20
|
+
from: hop.from,
|
|
21
|
+
to: hop.to,
|
|
22
|
+
event: hop.event,
|
|
23
|
+
...hop.firedArm !== void 0 ? { firedArm: hop.firedArm } : {},
|
|
24
|
+
...hop.guardResult !== void 0 ? { guardResult: hop.guardResult } : {},
|
|
25
|
+
effects: hop.effects,
|
|
26
|
+
...hop.observed !== void 0 ? { observed: hop.observed } : {}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function createLocalFold(opts) {
|
|
31
|
+
return {
|
|
32
|
+
foldLocalResponse: async (response, traceHops, notify) => {
|
|
33
|
+
if (traceHops) traceResponse(opts.observer, response);
|
|
34
|
+
paintResponse(opts.store, opts.framesSince, response);
|
|
35
|
+
if (notify) opts.store.notify();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { createLocalFold, paintFrames, paintResponse, paintStates, traceResponse };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/client/channel-transport.ts
|
|
2
|
+
var CLOSED = "The host closed the channel before answering";
|
|
3
|
+
function createChannelTransport(channel, options = {}) {
|
|
4
|
+
let next = 1;
|
|
5
|
+
const pending = /* @__PURE__ */ new Map();
|
|
6
|
+
channel.onMessage((message) => {
|
|
7
|
+
if (message.almadarChannel !== "registered" && message.almadarChannel !== "reply") return;
|
|
8
|
+
const settle = pending.get(message.id);
|
|
9
|
+
if (!settle) return;
|
|
10
|
+
pending.delete(message.id);
|
|
11
|
+
settle(message);
|
|
12
|
+
});
|
|
13
|
+
let closed = false;
|
|
14
|
+
const closedReply = (id) => ({
|
|
15
|
+
almadarChannel: "reply",
|
|
16
|
+
id,
|
|
17
|
+
response: { success: false, transitioned: false, states: {}, emittedEvents: [], error: CLOSED }
|
|
18
|
+
});
|
|
19
|
+
channel.onClose?.(() => {
|
|
20
|
+
closed = true;
|
|
21
|
+
for (const [id, settle] of pending) settle(closedReply(id));
|
|
22
|
+
pending.clear();
|
|
23
|
+
});
|
|
24
|
+
const request = (build) => new Promise((resolve) => {
|
|
25
|
+
const id = next++;
|
|
26
|
+
if (closed) {
|
|
27
|
+
resolve(closedReply(id));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
pending.set(id, resolve);
|
|
31
|
+
channel.post(build(id));
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
hostsBrowserStore: options.hostsBrowserStore === true,
|
|
35
|
+
async register(schema) {
|
|
36
|
+
const reply = await request((id) => ({ almadarChannel: "register", id, schemaName: schema.name }));
|
|
37
|
+
const held = reply.almadarChannel === "registered" && reply.held;
|
|
38
|
+
return { success: held, carriesCircuitState: false };
|
|
39
|
+
},
|
|
40
|
+
async unregister() {
|
|
41
|
+
pending.clear();
|
|
42
|
+
},
|
|
43
|
+
async send(orbitalName, req) {
|
|
44
|
+
const reply = await request((id) => ({ almadarChannel: "send", id, orbitalName, request: req }));
|
|
45
|
+
if (reply.almadarChannel !== "reply") {
|
|
46
|
+
return { success: false, transitioned: false, states: {}, emittedEvents: [], error: "The host answered out of protocol" };
|
|
47
|
+
}
|
|
48
|
+
return reply.response;
|
|
49
|
+
},
|
|
50
|
+
subscribe(onPush) {
|
|
51
|
+
return channel.onMessage((message) => {
|
|
52
|
+
if (message.almadarChannel === "push") onPush(message.emitted, message.target);
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
subscribeHostDispatches(onDispatch) {
|
|
56
|
+
return channel.onMessage((message) => {
|
|
57
|
+
if (message.almadarChannel === "dispatched") onDispatch(message.orbitalName, message.request, message.response);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function serveChannel(channel, transport, options) {
|
|
63
|
+
const stop = channel.onMessage((message) => {
|
|
64
|
+
if (message.almadarChannel === "register") {
|
|
65
|
+
channel.post({ almadarChannel: "registered", id: message.id, held: message.schemaName === options.schemaName });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (message.almadarChannel !== "send") return;
|
|
69
|
+
transport.send(message.orbitalName, message.request).then(
|
|
70
|
+
(response) => channel.post({ almadarChannel: "reply", id: message.id, response }),
|
|
71
|
+
(err) => channel.post({
|
|
72
|
+
almadarChannel: "reply",
|
|
73
|
+
id: message.id,
|
|
74
|
+
response: { success: false, transitioned: false, states: {}, emittedEvents: [], error: err.message }
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
push: (emitted, target) => channel.post({ almadarChannel: "push", emitted, target }),
|
|
80
|
+
pushDispatch: (orbitalName, request, response) => channel.post({ almadarChannel: "dispatched", orbitalName, request, response }),
|
|
81
|
+
stop
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function holdUntilServed(channel) {
|
|
85
|
+
let held = [];
|
|
86
|
+
const listeners = [];
|
|
87
|
+
channel.onMessage((message) => {
|
|
88
|
+
if (held !== void 0) held.push(message);
|
|
89
|
+
else listeners.forEach((l) => l(message));
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
post: (message) => channel.post(message),
|
|
93
|
+
onMessage(listener) {
|
|
94
|
+
listeners.push(listener);
|
|
95
|
+
if (held !== void 0) {
|
|
96
|
+
const early = held;
|
|
97
|
+
held = void 0;
|
|
98
|
+
early.forEach((m) => listeners.forEach((l) => l(m)));
|
|
99
|
+
}
|
|
100
|
+
return () => {
|
|
101
|
+
const i = listeners.indexOf(listener);
|
|
102
|
+
if (i >= 0) listeners.splice(i, 1);
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
...channel.onClose !== void 0 ? { onClose: channel.onClose.bind(channel) } : {}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { createChannelTransport, holdUntilServed, serveChannel };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createLogger } from '@almadar/logger';
|
|
2
|
+
|
|
3
|
+
// src/events/EventBus.ts
|
|
4
|
+
var log = createLogger("almadar:runtime:eventbus");
|
|
5
|
+
var EventBus = class {
|
|
6
|
+
listeners = /* @__PURE__ */ new Map();
|
|
7
|
+
/** Maximum recursion depth before circuit breaker activates (RCG-05) */
|
|
8
|
+
maxDepth;
|
|
9
|
+
/** Current emission depth for circular loop detection */
|
|
10
|
+
depth = 0;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
void options.debug;
|
|
13
|
+
this.maxDepth = options.maxDepth ?? 10;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Emit an event to all registered listeners.
|
|
17
|
+
*
|
|
18
|
+
* Includes circuit breaker (RCG-05): if emit is called recursively
|
|
19
|
+
* beyond `maxDepth`, the event is dropped and an error is logged.
|
|
20
|
+
* This prevents infinite loops from circular emit/listen chains.
|
|
21
|
+
*/
|
|
22
|
+
emit(type, payload, source, routingKey) {
|
|
23
|
+
if (this.depth >= this.maxDepth) {
|
|
24
|
+
log.error("circular event loop dropped", { type, depth: this.depth, maxDepth: this.maxDepth });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const event = {
|
|
28
|
+
type,
|
|
29
|
+
payload,
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
source
|
|
32
|
+
};
|
|
33
|
+
const deliveryKey = routingKey ?? type;
|
|
34
|
+
const primary = this.listeners.get(deliveryKey);
|
|
35
|
+
const secondary = routingKey !== void 0 && routingKey !== type ? this.listeners.get(type) : void 0;
|
|
36
|
+
const listenerCount = (primary?.size ?? 0) + (secondary?.size ?? 0);
|
|
37
|
+
if (listenerCount > 0) {
|
|
38
|
+
log.debug("emit", { type, listenerCount, depth: this.depth });
|
|
39
|
+
} else {
|
|
40
|
+
log.debug("emit no listeners", { type });
|
|
41
|
+
}
|
|
42
|
+
this.depth++;
|
|
43
|
+
try {
|
|
44
|
+
const delivered = /* @__PURE__ */ new Set();
|
|
45
|
+
const deliverTo = (set) => {
|
|
46
|
+
if (!set) return;
|
|
47
|
+
for (const listener of Array.from(set)) {
|
|
48
|
+
if (delivered.has(listener)) continue;
|
|
49
|
+
delivered.add(listener);
|
|
50
|
+
try {
|
|
51
|
+
listener(event);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
log.error("listener threw", { type, error: error instanceof Error ? error : String(error) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
deliverTo(primary);
|
|
58
|
+
deliverTo(secondary);
|
|
59
|
+
if (type !== "*") {
|
|
60
|
+
const wildcardListeners = this.listeners.get("*");
|
|
61
|
+
if (wildcardListeners) {
|
|
62
|
+
for (const listener of Array.from(wildcardListeners)) {
|
|
63
|
+
try {
|
|
64
|
+
listener(event);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
log.error("wildcard listener threw", { error: error instanceof Error ? error : String(error) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
this.depth--;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Subscribe to an event type
|
|
77
|
+
*/
|
|
78
|
+
on(type, listener) {
|
|
79
|
+
if (!this.listeners.has(type)) {
|
|
80
|
+
this.listeners.set(type, /* @__PURE__ */ new Set());
|
|
81
|
+
}
|
|
82
|
+
const listeners = this.listeners.get(type);
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
log.debug("subscribe", { type, total: listeners.size });
|
|
85
|
+
return () => {
|
|
86
|
+
listeners.delete(listener);
|
|
87
|
+
log.debug("unsubscribe", { type, remaining: listeners.size });
|
|
88
|
+
if (listeners.size === 0) {
|
|
89
|
+
this.listeners.delete(type);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Subscribe to ALL events (wildcard listener)
|
|
95
|
+
* Useful for event tracking, logging, debugging
|
|
96
|
+
*/
|
|
97
|
+
onAny(listener) {
|
|
98
|
+
return this.on("*", listener);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if there are listeners for an event type
|
|
102
|
+
*/
|
|
103
|
+
hasListeners(type) {
|
|
104
|
+
const listeners = this.listeners.get(type);
|
|
105
|
+
return listeners !== void 0 && listeners.size > 0;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get all registered event types
|
|
109
|
+
*/
|
|
110
|
+
getRegisteredEvents() {
|
|
111
|
+
return Array.from(this.listeners.keys());
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Clear all listeners
|
|
115
|
+
*/
|
|
116
|
+
clear() {
|
|
117
|
+
log.debug("clear", { eventTypeCount: this.listeners.size });
|
|
118
|
+
this.listeners.clear();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get listener count for an event type (for testing)
|
|
122
|
+
*/
|
|
123
|
+
getListenerCount(type) {
|
|
124
|
+
return this.listeners.get(type)?.size ?? 0;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
function createEventBus(options) {
|
|
128
|
+
return new EventBus(options);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { EventBus, createEventBus };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/service/service-provider.ts
|
|
2
|
+
var ServiceCallError = class extends Error {
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "ServiceCallError";
|
|
7
|
+
}
|
|
8
|
+
code;
|
|
9
|
+
};
|
|
10
|
+
var providers = /* @__PURE__ */ new Map();
|
|
11
|
+
function registerServiceProvider(service, provider) {
|
|
12
|
+
providers.set(service, provider);
|
|
13
|
+
}
|
|
14
|
+
function unregisterServiceProvider(service) {
|
|
15
|
+
providers.delete(service);
|
|
16
|
+
}
|
|
17
|
+
function registeredServices() {
|
|
18
|
+
return [...providers.keys()];
|
|
19
|
+
}
|
|
20
|
+
async function callRegisteredService(service, action, params, context) {
|
|
21
|
+
const provider = providers.get(service);
|
|
22
|
+
if (provider === void 0) throw new Error(`Unknown integration: ${service}`);
|
|
23
|
+
return provider(action, params ?? {}, context);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { ServiceCallError, callRegisteredService, registerServiceProvider, registeredServices, unregisterServiceProvider };
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createLogger } from '@almadar/logger';
|
|
2
|
+
import { RowQueryPageSchema, OrbitalRegisterResponseSchema } from '@almadar/core';
|
|
3
|
+
|
|
4
|
+
// src/client/EventTransport.ts
|
|
5
|
+
var log = createLogger("almadar:runtime:event-transport");
|
|
6
|
+
function deriveCarriesCircuitState(topology) {
|
|
7
|
+
return topology !== "stateful";
|
|
8
|
+
}
|
|
9
|
+
async function authHeaders(getAccessToken) {
|
|
10
|
+
const headers = { "Content-Type": "application/json" };
|
|
11
|
+
const token = getAccessToken ? await getAccessToken() : void 0;
|
|
12
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
13
|
+
return headers;
|
|
14
|
+
}
|
|
15
|
+
function deriveEventsUrl(serverUrl) {
|
|
16
|
+
const trimmed = serverUrl.replace(/\/+$/, "");
|
|
17
|
+
const apiRoot = trimmed.replace(/\/[^/]*$/, "");
|
|
18
|
+
return `${apiRoot}/events`;
|
|
19
|
+
}
|
|
20
|
+
function isBusPushEnvelope(value) {
|
|
21
|
+
return value.type === "bus" && typeof value.event === "string";
|
|
22
|
+
}
|
|
23
|
+
function isDispatchPushEnvelope(value) {
|
|
24
|
+
return value.type === "dispatch" && typeof value.orbitalName === "string" && value.request !== void 0 && value.response !== void 0;
|
|
25
|
+
}
|
|
26
|
+
var EVENT_SOURCE_CLOSED = 2;
|
|
27
|
+
var REOPEN_DELAY_MS = 1e3;
|
|
28
|
+
var pushChannels = /* @__PURE__ */ new Map();
|
|
29
|
+
function parseFrameNumber(id) {
|
|
30
|
+
const dash = id.lastIndexOf("-");
|
|
31
|
+
if (dash <= 0) return void 0;
|
|
32
|
+
const n = Number(id.slice(dash + 1));
|
|
33
|
+
return Number.isInteger(n) ? { boot: id.slice(0, dash), n } : void 0;
|
|
34
|
+
}
|
|
35
|
+
var lastFrameByClient = /* @__PURE__ */ new Map();
|
|
36
|
+
function acquirePushChannel(key, resolveUrl, resumeKey, subscriber) {
|
|
37
|
+
let channel = pushChannels.get(key);
|
|
38
|
+
if (channel === void 0) {
|
|
39
|
+
const created = { source: void 0, subscribers: /* @__PURE__ */ new Set(), closed: false };
|
|
40
|
+
const open = async () => {
|
|
41
|
+
let url;
|
|
42
|
+
try {
|
|
43
|
+
url = await resolveUrl();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error("push:open-failed", { error: err instanceof Error ? err.message : String(err) });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (created.closed) return;
|
|
49
|
+
const source = new EventSource(url);
|
|
50
|
+
created.source = source;
|
|
51
|
+
source.onmessage = (ev) => {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(ev.data);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
log.warn("push:parse-failed", { error: err instanceof Error ? err.message : String(err) });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const number = ev.lastEventId ? parseFrameNumber(ev.lastEventId) : void 0;
|
|
60
|
+
if (number !== void 0 && resumeKey !== void 0) {
|
|
61
|
+
const seen = lastFrameByClient.get(resumeKey);
|
|
62
|
+
if (seen !== void 0 && seen.boot === number.boot && number.n <= seen.n) return;
|
|
63
|
+
lastFrameByClient.set(resumeKey, number);
|
|
64
|
+
}
|
|
65
|
+
for (const sub of created.subscribers) sub(parsed);
|
|
66
|
+
};
|
|
67
|
+
source.onerror = () => {
|
|
68
|
+
log.warn("push:connection-error", { channel: key });
|
|
69
|
+
if (source.readyState === EVENT_SOURCE_CLOSED && !created.closed) {
|
|
70
|
+
created.source = void 0;
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
if (!created.closed) void open();
|
|
73
|
+
}, REOPEN_DELAY_MS);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
pushChannels.set(key, created);
|
|
78
|
+
void open();
|
|
79
|
+
channel = created;
|
|
80
|
+
}
|
|
81
|
+
channel.subscribers.add(subscriber);
|
|
82
|
+
return () => {
|
|
83
|
+
channel.subscribers.delete(subscriber);
|
|
84
|
+
if (channel.subscribers.size === 0) {
|
|
85
|
+
channel.closed = true;
|
|
86
|
+
channel.source?.close();
|
|
87
|
+
pushChannels.delete(key);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
var SessionEndedError = class extends Error {
|
|
92
|
+
constructor(message) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "SessionEndedError";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var TransportStatusError = class extends Error {
|
|
98
|
+
constructor(status, bodyText) {
|
|
99
|
+
super(`the server answered ${status}${bodyText.length > 0 ? `: ${bodyText}` : " with no body"}`);
|
|
100
|
+
this.status = status;
|
|
101
|
+
this.bodyText = bodyText;
|
|
102
|
+
this.refusal = parseRefusal(bodyText);
|
|
103
|
+
this.name = "TransportStatusError";
|
|
104
|
+
}
|
|
105
|
+
status;
|
|
106
|
+
bodyText;
|
|
107
|
+
/** The `{ error, code }` body when the answer carried one. */
|
|
108
|
+
refusal;
|
|
109
|
+
};
|
|
110
|
+
function parseRefusal(text) {
|
|
111
|
+
if (text.length === 0) return void 0;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(text);
|
|
114
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
115
|
+
} catch {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var COMPRESSED_BODY_MIN_BYTES = 32 * 1024;
|
|
120
|
+
async function encodeJsonBody(value, compress) {
|
|
121
|
+
const text = JSON.stringify(value);
|
|
122
|
+
if (!compress || text.length < COMPRESSED_BODY_MIN_BYTES || typeof CompressionStream === "undefined") return { body: text };
|
|
123
|
+
const bytes = new TextEncoder().encode(text);
|
|
124
|
+
const source = new ReadableStream({ start(controller) {
|
|
125
|
+
controller.enqueue(bytes);
|
|
126
|
+
controller.close();
|
|
127
|
+
} });
|
|
128
|
+
const gzipped = source.pipeThrough(new CompressionStream("gzip"));
|
|
129
|
+
return { body: new Uint8Array(await new Response(gzipped).arrayBuffer()), encoding: "gzip" };
|
|
130
|
+
}
|
|
131
|
+
async function jsonPost(fetchFn, url, value, getAccessToken, compress) {
|
|
132
|
+
const headers = await authHeaders(getAccessToken);
|
|
133
|
+
const { body, encoding } = await encodeJsonBody(value, compress);
|
|
134
|
+
if (encoding) headers["Content-Encoding"] = encoding;
|
|
135
|
+
return fetchFn(url, { method: "POST", headers, body });
|
|
136
|
+
}
|
|
137
|
+
function createHttpTransport(options) {
|
|
138
|
+
const { serverUrl, getAccessToken } = options;
|
|
139
|
+
const fetchFn = options.fetch ?? fetch.bind(globalThis);
|
|
140
|
+
const compress = options.compressRequests === true;
|
|
141
|
+
let behavior;
|
|
142
|
+
const openPushChannel = (params, subscriber) => {
|
|
143
|
+
if (typeof EventSource === "undefined") return () => {
|
|
144
|
+
};
|
|
145
|
+
const eventsUrl = deriveEventsUrl(serverUrl);
|
|
146
|
+
const resumeKey = params["clientId"] !== void 0 ? `${eventsUrl}|${params["clientId"]}` : void 0;
|
|
147
|
+
const withResume = (search) => {
|
|
148
|
+
const seen = resumeKey !== void 0 ? lastFrameByClient.get(resumeKey) : void 0;
|
|
149
|
+
if (seen !== void 0) search.set("since", `${seen.boot}-${seen.n}`);
|
|
150
|
+
return `${eventsUrl}?${search.toString()}`;
|
|
151
|
+
};
|
|
152
|
+
const key = `${eventsUrl}?${new URLSearchParams(params).toString()}`;
|
|
153
|
+
const mintUrl = params["clientId"] !== void 0 ? `${eventsUrl}/ticket?${new URLSearchParams({ clientId: params["clientId"] }).toString()}` : `${eventsUrl}/ticket`;
|
|
154
|
+
const resolveUrl = async () => {
|
|
155
|
+
const reply = await fetchFn(mintUrl, { method: "POST", headers: await authHeaders(getAccessToken) });
|
|
156
|
+
if (!reply.ok) throw new Error(`the server refused a stream ticket: ${reply.status}`);
|
|
157
|
+
const body = await reply.json();
|
|
158
|
+
if (typeof body.ticket !== "string" || body.ticket.length === 0) throw new Error("the server sent no stream ticket");
|
|
159
|
+
return withResume(new URLSearchParams({ ...params, ticket: body.ticket }));
|
|
160
|
+
};
|
|
161
|
+
return acquirePushChannel(key, resolveUrl, resumeKey, subscriber);
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
async register(schema) {
|
|
165
|
+
behavior = schema.name;
|
|
166
|
+
try {
|
|
167
|
+
const res = await jsonPost(fetchFn, `${serverUrl}/register`, { schema }, getAccessToken, compress);
|
|
168
|
+
const parsed = OrbitalRegisterResponseSchema.safeParse(await res.json());
|
|
169
|
+
if (!parsed.success) {
|
|
170
|
+
log.error("register:malformed-response", { issues: JSON.stringify(parsed.error.issues) });
|
|
171
|
+
return { success: false, carriesCircuitState: deriveCarriesCircuitState(void 0) };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
success: parsed.data.success,
|
|
175
|
+
carriesCircuitState: deriveCarriesCircuitState(parsed.data.topology)
|
|
176
|
+
};
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof TypeError) {
|
|
179
|
+
log.warn("register:failed", { error: err.message });
|
|
180
|
+
} else {
|
|
181
|
+
log.error("register:failed", { error: err instanceof Error ? err : String(err) });
|
|
182
|
+
}
|
|
183
|
+
return { success: false, carriesCircuitState: true };
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
async unregister() {
|
|
187
|
+
try {
|
|
188
|
+
await fetchFn(`${serverUrl}/unregister`, { method: "DELETE", headers: await authHeaders(getAccessToken) });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
log.warn("unregister-failed", { error: error instanceof Error ? error.message : String(error) });
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async send(orbitalName, request) {
|
|
194
|
+
const res = await jsonPost(
|
|
195
|
+
fetchFn,
|
|
196
|
+
`${serverUrl}/${orbitalName}/events`,
|
|
197
|
+
request.behavior === void 0 && behavior !== void 0 ? { ...request, behavior } : request,
|
|
198
|
+
getAccessToken,
|
|
199
|
+
compress
|
|
200
|
+
);
|
|
201
|
+
const text = await res.text();
|
|
202
|
+
if (res.status === 409) {
|
|
203
|
+
const refusal = parseRefusal(text);
|
|
204
|
+
if (refusal?.code === "SESSION_ENDED") throw new SessionEndedError(refusal.error ?? "the session ended");
|
|
205
|
+
}
|
|
206
|
+
if (!res.ok && res.status !== 403) throw new TransportStatusError(res.status, text);
|
|
207
|
+
const body = JSON.parse(text);
|
|
208
|
+
return body;
|
|
209
|
+
},
|
|
210
|
+
async resolveRows(reference) {
|
|
211
|
+
const res = await jsonPost(fetchFn, `${serverUrl}/resolve-rows`, { ref: reference }, getAccessToken, compress);
|
|
212
|
+
if (!res.ok) throw new Error(`resolve-rows: the server answered ${res.status}`);
|
|
213
|
+
return RowQueryPageSchema.parse(await res.json());
|
|
214
|
+
},
|
|
215
|
+
subscribe(onPush, params = {}) {
|
|
216
|
+
return openPushChannel(params, (envelope) => {
|
|
217
|
+
if (isBusPushEnvelope(envelope)) onPush({ event: envelope.event, payload: envelope.payload, source: envelope.source }, envelope.target ?? "peers");
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
subscribeHostDispatches(onDispatch, params = {}) {
|
|
221
|
+
return openPushChannel(params, (envelope) => {
|
|
222
|
+
if (isDispatchPushEnvelope(envelope)) onDispatch(envelope.orbitalName, envelope.request, envelope.response);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function createInProcessTransport(evaluate, options = {}) {
|
|
228
|
+
return {
|
|
229
|
+
async register(schema) {
|
|
230
|
+
await options.onRegister?.(schema);
|
|
231
|
+
return { success: true, carriesCircuitState: options.carriesCircuitState ?? false };
|
|
232
|
+
},
|
|
233
|
+
async unregister() {
|
|
234
|
+
await options.onUnregister?.();
|
|
235
|
+
},
|
|
236
|
+
async send(orbitalName, request) {
|
|
237
|
+
return evaluate(orbitalName, request);
|
|
238
|
+
},
|
|
239
|
+
...options.resolveRows !== void 0 ? { resolveRows: options.resolveRows } : {}
|
|
240
|
+
// No `subscribe` — an in-process transport has no server to push from.
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export { COMPRESSED_BODY_MIN_BYTES, SessionEndedError, TransportStatusError, createHttpTransport, createInProcessTransport, deriveCarriesCircuitState, encodeJsonBody };
|