@graphrefly/ts 0.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/LICENSE +21 -0
- package/dist/adapters/index.cjs +9015 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.cts +678 -0
- package/dist/adapters/index.d.ts +678 -0
- package/dist/adapters/index.js +2894 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/observe-storage.cjs +260 -0
- package/dist/adapters/observe-storage.cjs.map +1 -0
- package/dist/adapters/observe-storage.d.cts +77 -0
- package/dist/adapters/observe-storage.d.ts +77 -0
- package/dist/adapters/observe-storage.js +161 -0
- package/dist/adapters/observe-storage.js.map +1 -0
- package/dist/append-log-DisvEReb.d.cts +189 -0
- package/dist/append-log-DzKMmjxx.d.ts +189 -0
- package/dist/chunk-2ILL7HQI.js +1189 -0
- package/dist/chunk-2ILL7HQI.js.map +1 -0
- package/dist/chunk-4VLAQHZI.js +330 -0
- package/dist/chunk-4VLAQHZI.js.map +1 -0
- package/dist/chunk-6PHB5CJS.js +229 -0
- package/dist/chunk-6PHB5CJS.js.map +1 -0
- package/dist/chunk-COHO25CZ.js +2449 -0
- package/dist/chunk-COHO25CZ.js.map +1 -0
- package/dist/chunk-D6BG7U6A.js +161 -0
- package/dist/chunk-D6BG7U6A.js.map +1 -0
- package/dist/chunk-DJC4CHLQ.js +818 -0
- package/dist/chunk-DJC4CHLQ.js.map +1 -0
- package/dist/chunk-DK554R7P.js +67 -0
- package/dist/chunk-DK554R7P.js.map +1 -0
- package/dist/chunk-DKP56CSW.js +322 -0
- package/dist/chunk-DKP56CSW.js.map +1 -0
- package/dist/chunk-G4M3NHRJ.js +688 -0
- package/dist/chunk-G4M3NHRJ.js.map +1 -0
- package/dist/chunk-GAJW5I5P.js +1 -0
- package/dist/chunk-GAJW5I5P.js.map +1 -0
- package/dist/chunk-GDL26N2T.js +282 -0
- package/dist/chunk-GDL26N2T.js.map +1 -0
- package/dist/chunk-GTPCOEOZ.js +741 -0
- package/dist/chunk-GTPCOEOZ.js.map +1 -0
- package/dist/chunk-H2HTHFVN.js +1 -0
- package/dist/chunk-H2HTHFVN.js.map +1 -0
- package/dist/chunk-HDRL4VQQ.js +218 -0
- package/dist/chunk-HDRL4VQQ.js.map +1 -0
- package/dist/chunk-I3IHOHCJ.js +2267 -0
- package/dist/chunk-I3IHOHCJ.js.map +1 -0
- package/dist/chunk-JIMUDQN3.js +965 -0
- package/dist/chunk-JIMUDQN3.js.map +1 -0
- package/dist/chunk-JR4IKCR2.js +462 -0
- package/dist/chunk-JR4IKCR2.js.map +1 -0
- package/dist/chunk-K3FXTC3I.js +165 -0
- package/dist/chunk-K3FXTC3I.js.map +1 -0
- package/dist/chunk-OOEN33FB.js +65 -0
- package/dist/chunk-OOEN33FB.js.map +1 -0
- package/dist/chunk-PKVBHGBM.js +554 -0
- package/dist/chunk-PKVBHGBM.js.map +1 -0
- package/dist/chunk-RIIOUTGM.js +1857 -0
- package/dist/chunk-RIIOUTGM.js.map +1 -0
- package/dist/chunk-SCRFRCGR.js +33 -0
- package/dist/chunk-SCRFRCGR.js.map +1 -0
- package/dist/chunk-SWTLEQL6.js +53 -0
- package/dist/chunk-SWTLEQL6.js.map +1 -0
- package/dist/chunk-SZAETNNA.js +1 -0
- package/dist/chunk-SZAETNNA.js.map +1 -0
- package/dist/chunk-TEW47IVR.js +49 -0
- package/dist/chunk-TEW47IVR.js.map +1 -0
- package/dist/chunk-Y3ZLRGHC.js +1482 -0
- package/dist/chunk-Y3ZLRGHC.js.map +1 -0
- package/dist/chunk-Z2ANQ6JW.js +149 -0
- package/dist/chunk-Z2ANQ6JW.js.map +1 -0
- package/dist/codec-BB7p8fJc.d.cts +38 -0
- package/dist/codec-BB7p8fJc.d.ts +38 -0
- package/dist/composition/index.cjs +2505 -0
- package/dist/composition/index.cjs.map +1 -0
- package/dist/composition/index.d.cts +95 -0
- package/dist/composition/index.d.ts +95 -0
- package/dist/composition/index.js +19 -0
- package/dist/composition/index.js.map +1 -0
- package/dist/core/index.cjs +2475 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +25 -0
- package/dist/core/index.d.ts +25 -0
- package/dist/core/index.js +74 -0
- package/dist/core/index.js.map +1 -0
- package/dist/cqrs/index.cjs +595 -0
- package/dist/cqrs/index.cjs.map +1 -0
- package/dist/cqrs/index.d.cts +200 -0
- package/dist/cqrs/index.d.ts +200 -0
- package/dist/cqrs/index.js +557 -0
- package/dist/cqrs/index.js.map +1 -0
- package/dist/data-structures/index.cjs +4277 -0
- package/dist/data-structures/index.cjs.map +1 -0
- package/dist/data-structures/index.d.cts +22 -0
- package/dist/data-structures/index.d.ts +22 -0
- package/dist/data-structures/index.js +31 -0
- package/dist/data-structures/index.js.map +1 -0
- package/dist/describe-BZb7AS2y.d.ts +57 -0
- package/dist/describe-Ciu8SUnN.d.cts +57 -0
- package/dist/graph/index.cjs +7832 -0
- package/dist/graph/index.cjs.map +1 -0
- package/dist/graph/index.d.cts +251 -0
- package/dist/graph/index.d.ts +251 -0
- package/dist/graph/index.js +131 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph-DfdF9zy4.d.ts +666 -0
- package/dist/graph-s23RbbFO.d.cts +666 -0
- package/dist/index-CAEmLCYF.d.cts +166 -0
- package/dist/index-SnzZu3x7.d.ts +166 -0
- package/dist/index.cjs +15409 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +582 -0
- package/dist/index.js.map +1 -0
- package/dist/messages-BZqrGGm1.d.cts +55 -0
- package/dist/messages-BZqrGGm1.d.ts +55 -0
- package/dist/messaging/index.cjs +818 -0
- package/dist/messaging/index.cjs.map +1 -0
- package/dist/messaging/index.d.cts +235 -0
- package/dist/messaging/index.d.ts +235 -0
- package/dist/messaging/index.js +767 -0
- package/dist/messaging/index.js.map +1 -0
- package/dist/observe-event-log-0SVsx7JG.d.cts +88 -0
- package/dist/observe-event-log-Dnm76owp.d.ts +88 -0
- package/dist/operators/index.cjs +3774 -0
- package/dist/operators/index.cjs.map +1 -0
- package/dist/operators/index.d.cts +308 -0
- package/dist/operators/index.d.ts +308 -0
- package/dist/operators/index.js +112 -0
- package/dist/operators/index.js.map +1 -0
- package/dist/orchestration/index.cjs +3541 -0
- package/dist/orchestration/index.cjs.map +1 -0
- package/dist/orchestration/index.d.cts +302 -0
- package/dist/orchestration/index.d.ts +302 -0
- package/dist/orchestration/index.js +1004 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/patterns/index.cjs +1045 -0
- package/dist/patterns/index.cjs.map +1 -0
- package/dist/patterns/index.d.cts +39 -0
- package/dist/patterns/index.d.ts +39 -0
- package/dist/patterns/index.js +33 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/reactive-collection-BxBqtIOE.d.cts +344 -0
- package/dist/reactive-collection-Cb1II1Fi.d.ts +344 -0
- package/dist/reactive-collection-storage-BNiEc-Gd.d.ts +217 -0
- package/dist/reactive-collection-storage-D-KmjE0J.d.cts +217 -0
- package/dist/reactive-map-BcB4KsQR.d.ts +256 -0
- package/dist/reactive-map-D-g3dv8I.d.cts +256 -0
- package/dist/read-through-B1CYPfq_.d.cts +82 -0
- package/dist/read-through-DwyJhGs1.d.ts +82 -0
- package/dist/render/index.cjs +250 -0
- package/dist/render/index.cjs.map +1 -0
- package/dist/render/index.d.cts +58 -0
- package/dist/render/index.d.ts +58 -0
- package/dist/render/index.js +20 -0
- package/dist/render/index.js.map +1 -0
- package/dist/resilience-Dig3Q7n0.d.cts +45 -0
- package/dist/resilience-Dig3Q7n0.d.ts +45 -0
- package/dist/semantic-memory-graph-BYoIlP35.d.cts +356 -0
- package/dist/semantic-memory-graph-CxyXLms3.d.ts +356 -0
- package/dist/solutions/index.cjs +3249 -0
- package/dist/solutions/index.cjs.map +1 -0
- package/dist/solutions/index.d.cts +520 -0
- package/dist/solutions/index.d.ts +520 -0
- package/dist/solutions/index.js +31 -0
- package/dist/solutions/index.js.map +1 -0
- package/dist/sources/browser.cjs +3095 -0
- package/dist/sources/browser.cjs.map +1 -0
- package/dist/sources/browser.d.cts +39 -0
- package/dist/sources/browser.d.ts +39 -0
- package/dist/sources/browser.js +137 -0
- package/dist/sources/browser.js.map +1 -0
- package/dist/sources/index.cjs +3021 -0
- package/dist/sources/index.cjs.map +1 -0
- package/dist/sources/index.d.cts +7 -0
- package/dist/sources/index.d.ts +7 -0
- package/dist/sources/index.js +63 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/sources/node.cjs +675 -0
- package/dist/sources/node.cjs.map +1 -0
- package/dist/sources/node.d.cts +109 -0
- package/dist/sources/node.d.ts +109 -0
- package/dist/sources/node.js +639 -0
- package/dist/sources/node.js.map +1 -0
- package/dist/storage/browser.cjs +777 -0
- package/dist/storage/browser.cjs.map +1 -0
- package/dist/storage/browser.d.cts +40 -0
- package/dist/storage/browser.d.ts +40 -0
- package/dist/storage/browser.js +275 -0
- package/dist/storage/browser.js.map +1 -0
- package/dist/storage/index.cjs +1989 -0
- package/dist/storage/index.cjs.map +1 -0
- package/dist/storage/index.d.cts +92 -0
- package/dist/storage/index.d.ts +92 -0
- package/dist/storage/index.js +161 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/node.cjs +898 -0
- package/dist/storage/node.cjs.map +1 -0
- package/dist/storage/node.d.cts +66 -0
- package/dist/storage/node.d.ts +66 -0
- package/dist/storage/node.js +402 -0
- package/dist/storage/node.js.map +1 -0
- package/dist/testing/index.cjs +95 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +20 -0
- package/dist/testing/index.d.ts +20 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-bNMSWKqF.d.cts +732 -0
- package/dist/types-h12mNYbq.d.ts +732 -0
- package/package.json +250 -0
|
@@ -0,0 +1,2894 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENTIC_MEMORY_RECORD_CHANGE_FORMAT,
|
|
3
|
+
AGENTIC_MEMORY_RECORD_SNAPSHOT_FORMAT,
|
|
4
|
+
AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION,
|
|
5
|
+
agenticMemoryRecordChangeFrame,
|
|
6
|
+
agenticMemoryRecordSnapshotFrame,
|
|
7
|
+
agenticMemoryRecordsSnapshotKey,
|
|
8
|
+
assertAgenticMemoryRecordChangeFrame,
|
|
9
|
+
assertAgenticMemoryRecordSnapshotFrame,
|
|
10
|
+
loadAgenticMemoryRecordsState,
|
|
11
|
+
openPersistentAgenticMemoryRecords,
|
|
12
|
+
openPersistentReactiveIndex,
|
|
13
|
+
openPersistentReactiveList,
|
|
14
|
+
openPersistentReactiveLog,
|
|
15
|
+
openPersistentReactiveMap,
|
|
16
|
+
persistAgenticMemoryRecords,
|
|
17
|
+
persistReactiveCollection
|
|
18
|
+
} from "../chunk-DJC4CHLQ.js";
|
|
19
|
+
import "../chunk-GTPCOEOZ.js";
|
|
20
|
+
import "../chunk-COHO25CZ.js";
|
|
21
|
+
import "../chunk-JIMUDQN3.js";
|
|
22
|
+
import "../chunk-K3FXTC3I.js";
|
|
23
|
+
import "../chunk-6PHB5CJS.js";
|
|
24
|
+
import "../chunk-Z2ANQ6JW.js";
|
|
25
|
+
import "../chunk-2ILL7HQI.js";
|
|
26
|
+
import {
|
|
27
|
+
nextRetryDelayMs,
|
|
28
|
+
retryPolicy,
|
|
29
|
+
shouldRetry
|
|
30
|
+
} from "../chunk-OOEN33FB.js";
|
|
31
|
+
import "../chunk-DKP56CSW.js";
|
|
32
|
+
import "../chunk-RIIOUTGM.js";
|
|
33
|
+
import "../chunk-G4M3NHRJ.js";
|
|
34
|
+
import "../chunk-PKVBHGBM.js";
|
|
35
|
+
import "../chunk-I3IHOHCJ.js";
|
|
36
|
+
import "../chunk-4VLAQHZI.js";
|
|
37
|
+
import {
|
|
38
|
+
depBatch
|
|
39
|
+
} from "../chunk-SWTLEQL6.js";
|
|
40
|
+
import {
|
|
41
|
+
errorPayload
|
|
42
|
+
} from "../chunk-DK554R7P.js";
|
|
43
|
+
|
|
44
|
+
// src/adapters/bridge.ts
|
|
45
|
+
var envelopeTypes = /* @__PURE__ */ new Set([
|
|
46
|
+
"start",
|
|
47
|
+
"data",
|
|
48
|
+
"ack",
|
|
49
|
+
"nack",
|
|
50
|
+
"status",
|
|
51
|
+
"error",
|
|
52
|
+
"close"
|
|
53
|
+
]);
|
|
54
|
+
var defaultAckTimeoutMs = 3e4;
|
|
55
|
+
var bridgeCommandSources = /* @__PURE__ */ new WeakMap();
|
|
56
|
+
var bridgeInboundTargets = /* @__PURE__ */ new WeakMap();
|
|
57
|
+
function wireBridgeIdempotencyKey(sessionId, seq) {
|
|
58
|
+
return `${sessionId}:${seq}`;
|
|
59
|
+
}
|
|
60
|
+
function wireBridgeEnvelope(input) {
|
|
61
|
+
if (typeof input.sessionId !== "string" || input.sessionId.length === 0) {
|
|
62
|
+
throw new RangeError("wireBridgeEnvelope: sessionId must be a non-empty string");
|
|
63
|
+
}
|
|
64
|
+
if (!envelopeTypes.has(input.type)) {
|
|
65
|
+
throw new RangeError("wireBridgeEnvelope: type is not recognized");
|
|
66
|
+
}
|
|
67
|
+
if (!isSafePositiveInteger(input.seq)) {
|
|
68
|
+
throw new RangeError("wireBridgeEnvelope: seq must be a positive integer");
|
|
69
|
+
}
|
|
70
|
+
const cursor = input.cursor ?? 0;
|
|
71
|
+
if (!isSafeNonNegativeInteger(cursor)) {
|
|
72
|
+
throw new RangeError("wireBridgeEnvelope: cursor must be a non-negative integer");
|
|
73
|
+
}
|
|
74
|
+
const attempt = input.attempt ?? 1;
|
|
75
|
+
if (!isSafePositiveInteger(attempt)) {
|
|
76
|
+
throw new RangeError("wireBridgeEnvelope: attempt must be a positive integer");
|
|
77
|
+
}
|
|
78
|
+
const maxAttempts = input.maxAttempts ?? attempt;
|
|
79
|
+
if (!isSafePositiveInteger(maxAttempts) || maxAttempts < attempt) {
|
|
80
|
+
throw new RangeError("wireBridgeEnvelope: maxAttempts must be >= attempt");
|
|
81
|
+
}
|
|
82
|
+
if (input.ackForSeq !== void 0 && !isSafePositiveInteger(input.ackForSeq)) {
|
|
83
|
+
throw new RangeError("wireBridgeEnvelope: ackForSeq must be a positive integer");
|
|
84
|
+
}
|
|
85
|
+
if ((input.type === "ack" || input.type === "nack") && input.ackForSeq === void 0) {
|
|
86
|
+
throw new RangeError(`wireBridgeEnvelope: ${input.type} envelope requires ackForSeq`);
|
|
87
|
+
}
|
|
88
|
+
if (input.idempotencyKey !== void 0 && input.idempotencyKey.length === 0) {
|
|
89
|
+
throw new RangeError("wireBridgeEnvelope: idempotencyKey must be a non-empty string");
|
|
90
|
+
}
|
|
91
|
+
const payloadError = validatePayloadForType(input.type, input.payload, "wireBridgeEnvelope");
|
|
92
|
+
if (payloadError !== void 0) {
|
|
93
|
+
throw new RangeError(payloadError);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
sessionId: input.sessionId,
|
|
97
|
+
type: input.type,
|
|
98
|
+
payload: input.payload,
|
|
99
|
+
metadata: {
|
|
100
|
+
seq: input.seq,
|
|
101
|
+
cursor,
|
|
102
|
+
idempotencyKey: input.idempotencyKey ?? wireBridgeIdempotencyKey(input.sessionId, input.seq),
|
|
103
|
+
attempt,
|
|
104
|
+
maxAttempts,
|
|
105
|
+
timestampMs: input.timestampMs,
|
|
106
|
+
ackForSeq: input.ackForSeq,
|
|
107
|
+
requestId: input.requestId
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function wireBridge(graph, opts) {
|
|
112
|
+
const name = opts.name ?? "wireBridge";
|
|
113
|
+
const policy = opts.retry ?? retryPolicy();
|
|
114
|
+
const command = graph.node([], null, {
|
|
115
|
+
name: `${name}/command`,
|
|
116
|
+
factory: "wireBridgeCommand"
|
|
117
|
+
});
|
|
118
|
+
const inboundCore = graph.node(
|
|
119
|
+
[],
|
|
120
|
+
null,
|
|
121
|
+
{
|
|
122
|
+
name: `${name}/inbound`,
|
|
123
|
+
factory: "wireBridgeInbound"
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
const inbound = guardedInboundNode(inboundCore, opts.sessionId);
|
|
127
|
+
const events = wireBridgeEventsNode(
|
|
128
|
+
graph,
|
|
129
|
+
command,
|
|
130
|
+
inboundCore,
|
|
131
|
+
name,
|
|
132
|
+
opts,
|
|
133
|
+
policy
|
|
134
|
+
);
|
|
135
|
+
const outbound = projectOutbound(graph, events, name);
|
|
136
|
+
const acks = projectAcks(graph, events, name);
|
|
137
|
+
const nacks = projectNacks(graph, events, name);
|
|
138
|
+
const status = projectStatus(graph, events, name, opts.sessionId);
|
|
139
|
+
const errors = projectErrors(graph, events, name);
|
|
140
|
+
const cursor = projectCursor(graph, events, name);
|
|
141
|
+
const attempts = projectAttempts(graph, events, name);
|
|
142
|
+
const bundle = {
|
|
143
|
+
sessionId: opts.sessionId,
|
|
144
|
+
command,
|
|
145
|
+
outbound,
|
|
146
|
+
inbound,
|
|
147
|
+
events,
|
|
148
|
+
acks,
|
|
149
|
+
nacks,
|
|
150
|
+
status,
|
|
151
|
+
errors,
|
|
152
|
+
cursor,
|
|
153
|
+
attempts,
|
|
154
|
+
start: () => command.down([["DATA", { kind: "start" }]]),
|
|
155
|
+
send: (payload, sendOpts) => command.down([
|
|
156
|
+
[
|
|
157
|
+
"DATA",
|
|
158
|
+
{
|
|
159
|
+
kind: "send",
|
|
160
|
+
payload,
|
|
161
|
+
idempotencyKey: sendOpts?.idempotencyKey,
|
|
162
|
+
requestId: sendOpts?.requestId
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
]),
|
|
166
|
+
ack: (ackForSeq, ackOpts) => command.down([
|
|
167
|
+
[
|
|
168
|
+
"DATA",
|
|
169
|
+
{
|
|
170
|
+
kind: "ack",
|
|
171
|
+
ackForSeq,
|
|
172
|
+
idempotencyKey: ackOpts?.idempotencyKey,
|
|
173
|
+
requestId: ackOpts?.requestId
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
]),
|
|
177
|
+
nack: (ackForSeq, error, nackOpts) => command.down([
|
|
178
|
+
[
|
|
179
|
+
"DATA",
|
|
180
|
+
{
|
|
181
|
+
kind: "nack",
|
|
182
|
+
ackForSeq,
|
|
183
|
+
error,
|
|
184
|
+
idempotencyKey: nackOpts?.idempotencyKey,
|
|
185
|
+
requestId: nackOpts?.requestId
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
]),
|
|
189
|
+
close: (reason, closeOpts) => command.down([
|
|
190
|
+
["DATA", { kind: "close", reason, idempotencyKey: closeOpts?.idempotencyKey }]
|
|
191
|
+
])
|
|
192
|
+
};
|
|
193
|
+
bridgeCommandSources.set(bundle, { sources: [] });
|
|
194
|
+
bridgeInboundTargets.set(bundle, inboundCore);
|
|
195
|
+
return bundle;
|
|
196
|
+
}
|
|
197
|
+
function remoteCall(graph, bridge, opts = {}) {
|
|
198
|
+
const name = opts.name ?? "remoteCall";
|
|
199
|
+
const timeouts = graph.node([], null, {
|
|
200
|
+
name: `${name}/timeouts`,
|
|
201
|
+
factory: "remoteCallTimeouts",
|
|
202
|
+
completeWhenDepsComplete: false,
|
|
203
|
+
errorWhenDepsError: false
|
|
204
|
+
});
|
|
205
|
+
const responses = remoteCallResponsesNode(graph, bridge.events, timeouts, name);
|
|
206
|
+
const results = remoteCallResultsNode(graph, responses, name);
|
|
207
|
+
const status = remoteCallStatusNode(graph, bridge.events, responses, timeouts, name);
|
|
208
|
+
const errors = remoteCallErrorsNode(graph, responses, timeouts, bridge.events, name);
|
|
209
|
+
return {
|
|
210
|
+
responses,
|
|
211
|
+
results,
|
|
212
|
+
status,
|
|
213
|
+
errors,
|
|
214
|
+
timeouts,
|
|
215
|
+
call(operation, requestId, payload, callOpts) {
|
|
216
|
+
const request = remoteCallRequest(operation, requestId, payload);
|
|
217
|
+
bridge.command.down([
|
|
218
|
+
[
|
|
219
|
+
"DATA",
|
|
220
|
+
{
|
|
221
|
+
kind: "send",
|
|
222
|
+
payload: request,
|
|
223
|
+
idempotencyKey: callOpts?.idempotencyKey,
|
|
224
|
+
requestId
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
]);
|
|
228
|
+
return request;
|
|
229
|
+
},
|
|
230
|
+
timeout(requestId, operation, error) {
|
|
231
|
+
const timeout = remoteCallTimeout(requestId, operation, error);
|
|
232
|
+
timeouts.down([["DATA", timeout]]);
|
|
233
|
+
return timeout;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function remoteResponderHandler(operation, handle) {
|
|
238
|
+
if (operation.length === 0) {
|
|
239
|
+
throw new RangeError("remoteResponderHandler: operation must be non-empty");
|
|
240
|
+
}
|
|
241
|
+
if (typeof handle !== "function") {
|
|
242
|
+
throw new TypeError("remoteResponderHandler: handle must be a function");
|
|
243
|
+
}
|
|
244
|
+
return { operation, handle };
|
|
245
|
+
}
|
|
246
|
+
function remoteResponder(graph, bridge, opts = {}) {
|
|
247
|
+
const name = opts.name ?? "remoteResponder";
|
|
248
|
+
const handlers = normalizeRemoteHandlers(opts.handlers ?? []);
|
|
249
|
+
const topology = graph.topologyGroup({ name: `${name}.remoteResponder` });
|
|
250
|
+
const events = topology.add(
|
|
251
|
+
remoteResponderEventsNode(
|
|
252
|
+
graph,
|
|
253
|
+
wireBridgeInboundTarget(bridge),
|
|
254
|
+
name,
|
|
255
|
+
bridge.sessionId,
|
|
256
|
+
handlers,
|
|
257
|
+
opts.rejectUnknown === true
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
const responseCommands = topology.add(remoteResponderResponseCommandsNode(graph, events, name));
|
|
261
|
+
const requests = topology.add(remoteResponderRequestsNode(graph, events, name));
|
|
262
|
+
const status = topology.add(remoteResponderStatusNode(graph, events, name));
|
|
263
|
+
const errors = topology.add(remoteResponderErrorsNode(graph, events, name));
|
|
264
|
+
try {
|
|
265
|
+
attachWireBridgeCommandSource(bridge, responseCommands);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
topology.release({ reason: `${name}.remoteResponder.failedAttach` });
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
let released = false;
|
|
271
|
+
return {
|
|
272
|
+
events,
|
|
273
|
+
responseCommands,
|
|
274
|
+
requests,
|
|
275
|
+
status,
|
|
276
|
+
errors,
|
|
277
|
+
release() {
|
|
278
|
+
if (released) return;
|
|
279
|
+
detachWireBridgeCommandSource(bridge, responseCommands);
|
|
280
|
+
try {
|
|
281
|
+
topology.release({ reason: `${name}.remoteResponder.release` });
|
|
282
|
+
released = true;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
attachWireBridgeCommandSource(bridge, responseCommands);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function wireBridgeEventsNode(graph, command, inbound, name, opts, policy) {
|
|
291
|
+
const now = opts.now ?? Date.now;
|
|
292
|
+
return graph.node(
|
|
293
|
+
[command, inbound],
|
|
294
|
+
(ctx) => {
|
|
295
|
+
const state = initBridgeState(ctx);
|
|
296
|
+
const emitOutbound = (type, payload, commandOpts = {}, beforeEmit) => {
|
|
297
|
+
if (!isSafePositiveInteger(state.nextSeq)) {
|
|
298
|
+
ctx.down([
|
|
299
|
+
[
|
|
300
|
+
"DATA",
|
|
301
|
+
{
|
|
302
|
+
kind: "invalid",
|
|
303
|
+
error: `${opts.sessionId}: next outbound seq exceeded Number.MAX_SAFE_INTEGER`
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
]);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const seq = state.nextSeq;
|
|
310
|
+
let envelope;
|
|
311
|
+
try {
|
|
312
|
+
envelope = wireBridgeEnvelope({
|
|
313
|
+
sessionId: opts.sessionId,
|
|
314
|
+
type,
|
|
315
|
+
seq,
|
|
316
|
+
cursor: state.cursor,
|
|
317
|
+
payload,
|
|
318
|
+
idempotencyKey: commandOpts.idempotencyKey,
|
|
319
|
+
attempt: 1,
|
|
320
|
+
maxAttempts: policy.maxAttempts,
|
|
321
|
+
timestampMs: now(),
|
|
322
|
+
ackForSeq: commandOpts.ackForSeq,
|
|
323
|
+
requestId: commandOpts.requestId
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
ctx.down([
|
|
327
|
+
[
|
|
328
|
+
"DATA",
|
|
329
|
+
{
|
|
330
|
+
kind: "invalid",
|
|
331
|
+
error: error instanceof Error ? error.message : String(error)
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
]);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
state.nextSeq++;
|
|
338
|
+
beforeEmit?.();
|
|
339
|
+
ctx.down([["DATA", { kind: "outbound", envelope }]]);
|
|
340
|
+
if (shouldTrackAck(type)) armAckTimeout(ctx, state, envelope, opts, policy, now);
|
|
341
|
+
};
|
|
342
|
+
for (const raw of depBatch(ctx, 1) ?? []) {
|
|
343
|
+
processInbound(ctx, state, raw, opts.sessionId);
|
|
344
|
+
}
|
|
345
|
+
const inboundTerminal = ctx.terminal[1];
|
|
346
|
+
if (inboundTerminal !== false && inboundTerminal !== void 0 && !state.terminalReported) {
|
|
347
|
+
state.terminalReported = true;
|
|
348
|
+
const error = inboundTerminal === true ? `${opts.sessionId}: inbound protocol COMPLETE is local misuse; remote completion must arrive as a DATA envelope fact` : `${opts.sessionId}: inbound protocol ERROR ${String(
|
|
349
|
+
errorPayload(inboundTerminal)
|
|
350
|
+
)} is local misuse; remote errors must arrive as DATA envelope facts`;
|
|
351
|
+
ctx.down([["DATA", { kind: "invalid", error }]]);
|
|
352
|
+
}
|
|
353
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
354
|
+
const invalid = validateCommand(raw);
|
|
355
|
+
if (invalid !== void 0) {
|
|
356
|
+
ctx.down([["DATA", { kind: "invalid", error: invalid }]]);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const commandValue = raw;
|
|
360
|
+
switch (commandValue.kind) {
|
|
361
|
+
case "start":
|
|
362
|
+
emitOutbound("start", void 0, commandValue);
|
|
363
|
+
break;
|
|
364
|
+
case "send":
|
|
365
|
+
emitOutbound("data", { kind: "data", value: commandValue.payload }, commandValue);
|
|
366
|
+
break;
|
|
367
|
+
case "ack":
|
|
368
|
+
emitOutbound("ack", void 0, {
|
|
369
|
+
idempotencyKey: commandValue.idempotencyKey,
|
|
370
|
+
requestId: commandValue.requestId,
|
|
371
|
+
ackForSeq: commandValue.ackForSeq
|
|
372
|
+
});
|
|
373
|
+
break;
|
|
374
|
+
case "nack":
|
|
375
|
+
emitOutbound(
|
|
376
|
+
"nack",
|
|
377
|
+
{ kind: "error", error: errorPayload(commandValue.error) },
|
|
378
|
+
{
|
|
379
|
+
idempotencyKey: commandValue.idempotencyKey,
|
|
380
|
+
requestId: commandValue.requestId,
|
|
381
|
+
ackForSeq: commandValue.ackForSeq
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
break;
|
|
385
|
+
case "close":
|
|
386
|
+
emitOutbound(
|
|
387
|
+
"close",
|
|
388
|
+
{ kind: "close", reason: commandValue.reason },
|
|
389
|
+
commandValue,
|
|
390
|
+
() => {
|
|
391
|
+
for (const pending of state.pending.values()) {
|
|
392
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
393
|
+
}
|
|
394
|
+
state.pending.clear();
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: `${name}/events`,
|
|
403
|
+
factory: "wireBridgeEvents",
|
|
404
|
+
partial: true,
|
|
405
|
+
errorWhenDepsError: false,
|
|
406
|
+
completeWhenDepsComplete: false,
|
|
407
|
+
terminalAsRealInput: true
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
function initBridgeState(ctx) {
|
|
412
|
+
let state = ctx.state.get();
|
|
413
|
+
if (state === void 0) {
|
|
414
|
+
state = {
|
|
415
|
+
active: true,
|
|
416
|
+
cleanupInstalled: false,
|
|
417
|
+
nextSeq: 1,
|
|
418
|
+
cursor: 0,
|
|
419
|
+
remoteCursor: 0,
|
|
420
|
+
terminalReported: false,
|
|
421
|
+
pending: /* @__PURE__ */ new Map()
|
|
422
|
+
};
|
|
423
|
+
ctx.state.set(state);
|
|
424
|
+
}
|
|
425
|
+
state.active = true;
|
|
426
|
+
if (!state.cleanupInstalled) {
|
|
427
|
+
state.cleanupInstalled = true;
|
|
428
|
+
ctx.onDeactivation(() => {
|
|
429
|
+
state.active = false;
|
|
430
|
+
state.cleanupInstalled = false;
|
|
431
|
+
for (const pending of state.pending.values()) {
|
|
432
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
433
|
+
}
|
|
434
|
+
state.pending.clear();
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return state;
|
|
438
|
+
}
|
|
439
|
+
function attachWireBridgeCommandSource(bridge, source) {
|
|
440
|
+
const attached = bridgeCommandSources.get(bridge);
|
|
441
|
+
if (attached === void 0) {
|
|
442
|
+
throw new Error("remoteResponder: bridge command source registry missing");
|
|
443
|
+
}
|
|
444
|
+
const sources = attached.sources;
|
|
445
|
+
const previousSources = [...sources];
|
|
446
|
+
if (!sources.includes(source)) sources.push(source);
|
|
447
|
+
try {
|
|
448
|
+
bridge.command.replaceDeps([...sources], wireBridgeCommandSourceFn(sources.length));
|
|
449
|
+
} catch (error) {
|
|
450
|
+
sources.splice(0, sources.length, ...previousSources);
|
|
451
|
+
bridge.command.replaceDeps(
|
|
452
|
+
[...previousSources],
|
|
453
|
+
wireBridgeCommandSourceFn(previousSources.length)
|
|
454
|
+
);
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function detachWireBridgeCommandSource(bridge, source) {
|
|
459
|
+
const attached = bridgeCommandSources.get(bridge);
|
|
460
|
+
if (attached === void 0) {
|
|
461
|
+
throw new Error("remoteResponder: bridge command source registry missing");
|
|
462
|
+
}
|
|
463
|
+
const sources = attached.sources;
|
|
464
|
+
if (!sources.includes(source)) return;
|
|
465
|
+
const previousSources = [...sources];
|
|
466
|
+
const nextSources = sources.filter((candidate) => candidate !== source);
|
|
467
|
+
sources.splice(0, sources.length, ...nextSources);
|
|
468
|
+
try {
|
|
469
|
+
bridge.command.replaceDeps([...nextSources], wireBridgeCommandSourceFn(nextSources.length));
|
|
470
|
+
} catch (error) {
|
|
471
|
+
sources.splice(0, sources.length, ...previousSources);
|
|
472
|
+
bridge.command.replaceDeps(
|
|
473
|
+
[...previousSources],
|
|
474
|
+
wireBridgeCommandSourceFn(previousSources.length)
|
|
475
|
+
);
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function wireBridgeInboundTarget(bridge) {
|
|
480
|
+
const target = bridgeInboundTargets.get(bridge);
|
|
481
|
+
if (target === void 0) {
|
|
482
|
+
throw new Error("remoteResponder: bridge inbound target registry missing");
|
|
483
|
+
}
|
|
484
|
+
return target;
|
|
485
|
+
}
|
|
486
|
+
function wireBridgeCommandSourceFn(sourceCount) {
|
|
487
|
+
return (ctx) => {
|
|
488
|
+
for (let i = 0; i < sourceCount; i += 1) {
|
|
489
|
+
for (const command of depBatch(ctx, i) ?? []) {
|
|
490
|
+
ctx.down([["DATA", command]]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function remoteCallRequest(operation, requestId, payload) {
|
|
496
|
+
if (operation.length === 0) throw new RangeError("remoteCall: operation must be non-empty");
|
|
497
|
+
if (requestId.length === 0) throw new RangeError("remoteCall: requestId must be non-empty");
|
|
498
|
+
return { operation, requestId, payload };
|
|
499
|
+
}
|
|
500
|
+
function remoteCallTimeout(requestId, operation, error) {
|
|
501
|
+
if (requestId.length === 0)
|
|
502
|
+
throw new RangeError("remoteCall: timeout requestId must be non-empty");
|
|
503
|
+
if (error.length === 0) throw new RangeError("remoteCall: timeout error must be non-empty");
|
|
504
|
+
return { ...operation === void 0 ? {} : { operation }, requestId, error };
|
|
505
|
+
}
|
|
506
|
+
function remotePendingState() {
|
|
507
|
+
return { requestIds: /* @__PURE__ */ new Set(), bySeq: /* @__PURE__ */ new Map() };
|
|
508
|
+
}
|
|
509
|
+
function remotePendingContains(state, requestId) {
|
|
510
|
+
return state.requestIds.has(requestId);
|
|
511
|
+
}
|
|
512
|
+
function remotePendingDuplicate(state, request) {
|
|
513
|
+
const current = remotePendingTakeBySeqPreview(state, request.seq);
|
|
514
|
+
return current?.requestId !== request.requestId && state.requestIds.has(request.requestId);
|
|
515
|
+
}
|
|
516
|
+
function remotePendingTakeBySeqPreview(state, seq) {
|
|
517
|
+
return state.bySeq.get(seq);
|
|
518
|
+
}
|
|
519
|
+
function remotePendingInsert(state, request) {
|
|
520
|
+
const existingBySeq = state.bySeq.get(request.seq);
|
|
521
|
+
if (existingBySeq !== void 0) {
|
|
522
|
+
state.requestIds.delete(existingBySeq.requestId);
|
|
523
|
+
}
|
|
524
|
+
for (const [seq, pending] of state.bySeq) {
|
|
525
|
+
if (pending.requestId === request.requestId && seq !== request.seq) {
|
|
526
|
+
state.bySeq.delete(seq);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
state.requestIds.add(request.requestId);
|
|
531
|
+
state.bySeq.set(request.seq, request);
|
|
532
|
+
}
|
|
533
|
+
function remotePendingTakeByRequestId(state, requestId) {
|
|
534
|
+
if (!state.requestIds.delete(requestId)) return void 0;
|
|
535
|
+
let seq;
|
|
536
|
+
let found;
|
|
537
|
+
for (const [candidateSeq, request] of state.bySeq) {
|
|
538
|
+
if (request.requestId === requestId) {
|
|
539
|
+
seq = candidateSeq;
|
|
540
|
+
found = request;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (seq !== void 0) state.bySeq.delete(seq);
|
|
545
|
+
return found;
|
|
546
|
+
}
|
|
547
|
+
function remotePendingTakeBySeq(state, seq) {
|
|
548
|
+
const request = state.bySeq.get(seq);
|
|
549
|
+
if (request === void 0) return void 0;
|
|
550
|
+
state.bySeq.delete(seq);
|
|
551
|
+
state.requestIds.delete(request.requestId);
|
|
552
|
+
return request;
|
|
553
|
+
}
|
|
554
|
+
function remotePendingFromReceipt(state, envelope) {
|
|
555
|
+
const bySeq = remotePendingTakeBySeq(state, envelope.metadata.seq);
|
|
556
|
+
if (bySeq !== void 0) return bySeq;
|
|
557
|
+
const fallback = pendingRequestFromEnvelope(envelope);
|
|
558
|
+
if (fallback === void 0 || remotePendingContains(state, fallback.requestId)) {
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
561
|
+
return fallback;
|
|
562
|
+
}
|
|
563
|
+
function pendingRequestFromEnvelope(envelope) {
|
|
564
|
+
if (envelope.payload?.kind !== "data") return void 0;
|
|
565
|
+
const request = validateRemoteCallRequest(envelope.payload.value);
|
|
566
|
+
if (request === void 0) return void 0;
|
|
567
|
+
return {
|
|
568
|
+
operation: request.operation,
|
|
569
|
+
requestId: request.requestId,
|
|
570
|
+
seq: envelope.metadata.seq
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function validateRemoteCallRequest(value) {
|
|
574
|
+
if (!isRecord(value)) return void 0;
|
|
575
|
+
if (typeof value.operation !== "string" || value.operation.length === 0) return void 0;
|
|
576
|
+
if (typeof value.requestId !== "string" || value.requestId.length === 0) return void 0;
|
|
577
|
+
if (!("payload" in value)) return void 0;
|
|
578
|
+
return {
|
|
579
|
+
operation: value.operation,
|
|
580
|
+
requestId: value.requestId,
|
|
581
|
+
payload: value.payload
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function validateRemoteCallResponse(value) {
|
|
585
|
+
if (!isRecord(value)) return void 0;
|
|
586
|
+
if (typeof value.operation !== "string" || value.operation.length === 0) return void 0;
|
|
587
|
+
if (typeof value.requestId !== "string" || value.requestId.length === 0) return void 0;
|
|
588
|
+
if (value.kind === "result" && "payload" in value) {
|
|
589
|
+
return {
|
|
590
|
+
kind: "result",
|
|
591
|
+
operation: value.operation,
|
|
592
|
+
requestId: value.requestId,
|
|
593
|
+
payload: value.payload
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (value.kind === "error" && typeof value.error === "string" && value.error.length > 0) {
|
|
597
|
+
return {
|
|
598
|
+
kind: "error",
|
|
599
|
+
operation: value.operation,
|
|
600
|
+
requestId: value.requestId,
|
|
601
|
+
error: value.error
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
if (value.kind === "status" && typeof value.status === "string" && value.status.length > 0) {
|
|
605
|
+
return {
|
|
606
|
+
kind: "status",
|
|
607
|
+
operation: value.operation,
|
|
608
|
+
requestId: value.requestId,
|
|
609
|
+
status: value.status
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return void 0;
|
|
613
|
+
}
|
|
614
|
+
function remoteMalformedResponseRequestId(value) {
|
|
615
|
+
if (!isRecord(value)) return void 0;
|
|
616
|
+
return typeof value.requestId === "string" && value.requestId.length > 0 ? value.requestId : void 0;
|
|
617
|
+
}
|
|
618
|
+
function isRecord(value) {
|
|
619
|
+
return typeof value === "object" && value !== null;
|
|
620
|
+
}
|
|
621
|
+
function isThenable(value) {
|
|
622
|
+
return isRecord(value) && typeof value.then === "function";
|
|
623
|
+
}
|
|
624
|
+
function remoteCallResponseRequestId(response) {
|
|
625
|
+
return response.requestId;
|
|
626
|
+
}
|
|
627
|
+
function remoteCallResponseIsTerminal(response) {
|
|
628
|
+
return response.kind === "result" || response.kind === "error";
|
|
629
|
+
}
|
|
630
|
+
function remoteCallResponsesNode(graph, events, timeouts, name) {
|
|
631
|
+
return graph.node(
|
|
632
|
+
[events, timeouts],
|
|
633
|
+
(ctx) => {
|
|
634
|
+
const state = ctx.state.get() ?? {
|
|
635
|
+
pending: remotePendingState()
|
|
636
|
+
};
|
|
637
|
+
const ready = [];
|
|
638
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
639
|
+
const event = raw;
|
|
640
|
+
if (event.kind === "outbound") {
|
|
641
|
+
const request = pendingRequestFromEnvelope(event.envelope);
|
|
642
|
+
if (request === void 0) continue;
|
|
643
|
+
if (remotePendingDuplicate(state.pending, request)) continue;
|
|
644
|
+
remotePendingInsert(state.pending, request);
|
|
645
|
+
} else if (event.kind === "inbound" && event.envelope.payload?.kind === "data") {
|
|
646
|
+
const response = validateRemoteCallResponse(event.envelope.payload.value);
|
|
647
|
+
if (response === void 0) {
|
|
648
|
+
const requestId2 = remoteMalformedResponseRequestId(event.envelope.payload.value);
|
|
649
|
+
if (requestId2 !== void 0 && remotePendingContains(state.pending, requestId2)) {
|
|
650
|
+
remotePendingTakeByRequestId(state.pending, requestId2);
|
|
651
|
+
}
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const requestId = remoteCallResponseRequestId(response);
|
|
655
|
+
if (remotePendingContains(state.pending, requestId)) {
|
|
656
|
+
if (remoteCallResponseIsTerminal(response)) {
|
|
657
|
+
remotePendingTakeByRequestId(state.pending, requestId);
|
|
658
|
+
}
|
|
659
|
+
ready.push(response);
|
|
660
|
+
}
|
|
661
|
+
} else if (event.kind === "nack") {
|
|
662
|
+
remotePendingFromReceipt(state.pending, event.outbound);
|
|
663
|
+
} else if (event.kind === "exhausted") {
|
|
664
|
+
remotePendingTakeBySeq(state.pending, event.seq);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const raw of depBatch(ctx, 1) ?? []) {
|
|
668
|
+
const timeout = raw;
|
|
669
|
+
remotePendingTakeByRequestId(state.pending, timeout.requestId);
|
|
670
|
+
}
|
|
671
|
+
ctx.state.set(state);
|
|
672
|
+
for (const response of ready) ctx.down([["DATA", response]]);
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: `${name}/responses`,
|
|
676
|
+
factory: "remoteCallResponses",
|
|
677
|
+
partial: true,
|
|
678
|
+
completeWhenDepsComplete: false,
|
|
679
|
+
errorWhenDepsError: false
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
function remoteCallResultsNode(graph, responses, name) {
|
|
684
|
+
return graph.node(
|
|
685
|
+
[responses],
|
|
686
|
+
(ctx) => {
|
|
687
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
688
|
+
const response = raw;
|
|
689
|
+
if (response.kind === "result") {
|
|
690
|
+
ctx.down([
|
|
691
|
+
[
|
|
692
|
+
"DATA",
|
|
693
|
+
{
|
|
694
|
+
operation: response.operation,
|
|
695
|
+
requestId: response.requestId,
|
|
696
|
+
payload: response.payload
|
|
697
|
+
}
|
|
698
|
+
]
|
|
699
|
+
]);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
{ name: `${name}/results`, factory: "remoteCallResults", completeWhenDepsComplete: false }
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
function remoteCallStatusNode(graph, events, responses, timeouts, name) {
|
|
707
|
+
return graph.node(
|
|
708
|
+
[events, responses, timeouts],
|
|
709
|
+
(ctx) => {
|
|
710
|
+
const state = ctx.state.get() ?? {
|
|
711
|
+
status: { state: "idle", pending: 0, completed: 0, errors: 0, timeouts: 0 },
|
|
712
|
+
pending: remotePendingState()
|
|
713
|
+
};
|
|
714
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
715
|
+
const event = raw;
|
|
716
|
+
if (event.kind === "outbound") {
|
|
717
|
+
const request = pendingRequestFromEnvelope(event.envelope);
|
|
718
|
+
if (request !== void 0) {
|
|
719
|
+
if (remotePendingDuplicate(state.pending, request)) {
|
|
720
|
+
state.status = {
|
|
721
|
+
...state.status,
|
|
722
|
+
state: "errored",
|
|
723
|
+
operation: request.operation,
|
|
724
|
+
requestId: request.requestId,
|
|
725
|
+
errors: state.status.errors + 1
|
|
726
|
+
};
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
remotePendingInsert(state.pending, request);
|
|
730
|
+
state.status = {
|
|
731
|
+
...state.status,
|
|
732
|
+
state: "requested",
|
|
733
|
+
operation: request.operation,
|
|
734
|
+
requestId: request.requestId
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
} else if (event.kind === "invalid" || event.kind === "session-mismatch" || event.kind === "out-of-order" || event.kind === "late-receipt") {
|
|
738
|
+
state.status = {
|
|
739
|
+
...state.status,
|
|
740
|
+
state: "bridge-errored",
|
|
741
|
+
errors: state.status.errors + 1
|
|
742
|
+
};
|
|
743
|
+
} else if (event.kind === "nack") {
|
|
744
|
+
const request = remotePendingFromReceipt(state.pending, event.outbound);
|
|
745
|
+
state.status = {
|
|
746
|
+
...state.status,
|
|
747
|
+
state: "bridge-errored",
|
|
748
|
+
...request === void 0 ? {} : { operation: request.operation, requestId: request.requestId },
|
|
749
|
+
errors: state.status.errors + 1
|
|
750
|
+
};
|
|
751
|
+
} else if (event.kind === "exhausted") {
|
|
752
|
+
const request = remotePendingTakeBySeq(state.pending, event.seq);
|
|
753
|
+
state.status = {
|
|
754
|
+
...state.status,
|
|
755
|
+
state: "bridge-errored",
|
|
756
|
+
...request === void 0 ? {} : { operation: request.operation, requestId: request.requestId },
|
|
757
|
+
errors: state.status.errors + 1
|
|
758
|
+
};
|
|
759
|
+
} else if (event.kind === "inbound" && event.envelope.payload?.kind === "data") {
|
|
760
|
+
const response = validateRemoteCallResponse(event.envelope.payload.value);
|
|
761
|
+
if (response === void 0) {
|
|
762
|
+
const requestId = remoteMalformedResponseRequestId(event.envelope.payload.value);
|
|
763
|
+
if (requestId !== void 0) {
|
|
764
|
+
const request = remotePendingTakeByRequestId(state.pending, requestId);
|
|
765
|
+
if (request !== void 0) {
|
|
766
|
+
state.status = {
|
|
767
|
+
...state.status,
|
|
768
|
+
state: "errored",
|
|
769
|
+
operation: request.operation,
|
|
770
|
+
requestId,
|
|
771
|
+
errors: state.status.errors + 1
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
for (const raw of depBatch(ctx, 1) ?? []) {
|
|
779
|
+
const response = raw;
|
|
780
|
+
if (response.kind === "result") {
|
|
781
|
+
remotePendingTakeByRequestId(state.pending, response.requestId);
|
|
782
|
+
state.status = {
|
|
783
|
+
...state.status,
|
|
784
|
+
state: "responded",
|
|
785
|
+
operation: response.operation,
|
|
786
|
+
requestId: response.requestId,
|
|
787
|
+
completed: state.status.completed + 1
|
|
788
|
+
};
|
|
789
|
+
} else if (response.kind === "error") {
|
|
790
|
+
remotePendingTakeByRequestId(state.pending, response.requestId);
|
|
791
|
+
state.status = {
|
|
792
|
+
...state.status,
|
|
793
|
+
state: "errored",
|
|
794
|
+
operation: response.operation,
|
|
795
|
+
requestId: response.requestId,
|
|
796
|
+
errors: state.status.errors + 1
|
|
797
|
+
};
|
|
798
|
+
} else {
|
|
799
|
+
state.status = {
|
|
800
|
+
...state.status,
|
|
801
|
+
operation: response.operation,
|
|
802
|
+
requestId: response.requestId
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
for (const raw of depBatch(ctx, 2) ?? []) {
|
|
807
|
+
const timeout = raw;
|
|
808
|
+
remotePendingTakeByRequestId(state.pending, timeout.requestId);
|
|
809
|
+
state.status = {
|
|
810
|
+
...state.status,
|
|
811
|
+
state: "timed-out",
|
|
812
|
+
operation: timeout.operation,
|
|
813
|
+
requestId: timeout.requestId,
|
|
814
|
+
errors: state.status.errors + 1,
|
|
815
|
+
timeouts: state.status.timeouts + 1
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
state.status = { ...state.status, pending: state.pending.requestIds.size };
|
|
819
|
+
ctx.state.set(state);
|
|
820
|
+
ctx.down([["DATA", state.status]]);
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
name: `${name}/status`,
|
|
824
|
+
factory: "remoteCallStatus",
|
|
825
|
+
partial: true,
|
|
826
|
+
completeWhenDepsComplete: false,
|
|
827
|
+
errorWhenDepsError: false
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
function remoteCallErrorsNode(graph, responses, timeouts, events, name) {
|
|
832
|
+
return graph.node(
|
|
833
|
+
[responses, timeouts, events],
|
|
834
|
+
(ctx) => {
|
|
835
|
+
const pending = ctx.state.get() ?? remotePendingState();
|
|
836
|
+
for (const raw of depBatch(ctx, 2) ?? []) {
|
|
837
|
+
const event = raw;
|
|
838
|
+
if (event.kind === "outbound") {
|
|
839
|
+
const request = pendingRequestFromEnvelope(event.envelope);
|
|
840
|
+
if (request !== void 0) {
|
|
841
|
+
if (remotePendingDuplicate(pending, request)) {
|
|
842
|
+
ctx.down([
|
|
843
|
+
[
|
|
844
|
+
"DATA",
|
|
845
|
+
{
|
|
846
|
+
operation: request.operation,
|
|
847
|
+
requestId: request.requestId,
|
|
848
|
+
error: `remoteCall: duplicate in-flight requestId '${request.requestId}'`
|
|
849
|
+
}
|
|
850
|
+
]
|
|
851
|
+
]);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
remotePendingInsert(pending, request);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
859
|
+
const response = raw;
|
|
860
|
+
if (response.kind === "error") {
|
|
861
|
+
remotePendingTakeByRequestId(pending, response.requestId);
|
|
862
|
+
ctx.down([
|
|
863
|
+
[
|
|
864
|
+
"DATA",
|
|
865
|
+
{
|
|
866
|
+
operation: response.operation,
|
|
867
|
+
requestId: response.requestId,
|
|
868
|
+
error: response.error
|
|
869
|
+
}
|
|
870
|
+
]
|
|
871
|
+
]);
|
|
872
|
+
} else if (response.kind === "result") {
|
|
873
|
+
remotePendingTakeByRequestId(pending, response.requestId);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
for (const raw of depBatch(ctx, 1) ?? []) {
|
|
877
|
+
const timeout = raw;
|
|
878
|
+
remotePendingTakeByRequestId(pending, timeout.requestId);
|
|
879
|
+
ctx.down([
|
|
880
|
+
[
|
|
881
|
+
"DATA",
|
|
882
|
+
{ operation: timeout.operation, requestId: timeout.requestId, error: timeout.error }
|
|
883
|
+
]
|
|
884
|
+
]);
|
|
885
|
+
}
|
|
886
|
+
for (const raw of depBatch(ctx, 2) ?? []) {
|
|
887
|
+
const event = raw;
|
|
888
|
+
if (event.kind === "inbound" && event.envelope.payload?.kind === "data") {
|
|
889
|
+
const response = validateRemoteCallResponse(event.envelope.payload.value);
|
|
890
|
+
if (response === void 0) {
|
|
891
|
+
const requestId = remoteMalformedResponseRequestId(event.envelope.payload.value);
|
|
892
|
+
const request = requestId === void 0 ? void 0 : remotePendingTakeByRequestId(pending, requestId);
|
|
893
|
+
if (request === void 0) continue;
|
|
894
|
+
ctx.down([
|
|
895
|
+
[
|
|
896
|
+
"DATA",
|
|
897
|
+
{
|
|
898
|
+
operation: request.operation,
|
|
899
|
+
requestId: request.requestId,
|
|
900
|
+
error: "remoteCall: response payload is malformed"
|
|
901
|
+
}
|
|
902
|
+
]
|
|
903
|
+
]);
|
|
904
|
+
}
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (event.kind === "nack") {
|
|
908
|
+
const request = remotePendingFromReceipt(pending, event.outbound);
|
|
909
|
+
ctx.down([
|
|
910
|
+
[
|
|
911
|
+
"DATA",
|
|
912
|
+
{
|
|
913
|
+
operation: request?.operation,
|
|
914
|
+
requestId: request?.requestId,
|
|
915
|
+
error: String(event.error)
|
|
916
|
+
}
|
|
917
|
+
]
|
|
918
|
+
]);
|
|
919
|
+
} else if (event.kind === "exhausted") {
|
|
920
|
+
const request = remotePendingTakeBySeq(pending, event.seq);
|
|
921
|
+
ctx.down([
|
|
922
|
+
[
|
|
923
|
+
"DATA",
|
|
924
|
+
{
|
|
925
|
+
operation: request?.operation,
|
|
926
|
+
requestId: request?.requestId,
|
|
927
|
+
error: String(event.error)
|
|
928
|
+
}
|
|
929
|
+
]
|
|
930
|
+
]);
|
|
931
|
+
} else if (event.kind === "invalid") {
|
|
932
|
+
ctx.down([["DATA", { error: event.error }]]);
|
|
933
|
+
} else if (event.kind === "session-mismatch" || event.kind === "out-of-order" || event.kind === "late-receipt") {
|
|
934
|
+
ctx.down([["DATA", { error: remoteCallBridgeErrorMessage(event) }]]);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
ctx.state.set(pending);
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: `${name}/errors`,
|
|
941
|
+
factory: "remoteCallErrors",
|
|
942
|
+
partial: true,
|
|
943
|
+
completeWhenDepsComplete: false,
|
|
944
|
+
errorWhenDepsError: false
|
|
945
|
+
}
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
function remoteCallBridgeErrorMessage(event) {
|
|
949
|
+
if (event.kind === "session-mismatch") {
|
|
950
|
+
return `remoteCall: inbound session ${event.actual} did not match expected ${event.expected}`;
|
|
951
|
+
}
|
|
952
|
+
if (event.kind === "out-of-order") {
|
|
953
|
+
return `remoteCall: inbound seq ${event.seq} arrived before expected seq ${event.expected}`;
|
|
954
|
+
}
|
|
955
|
+
if (event.kind === "late-receipt") {
|
|
956
|
+
return `remoteCall: late ${event.receipt} for unknown or completed ackForSeq ${event.ackForSeq}`;
|
|
957
|
+
}
|
|
958
|
+
return "remoteCall: bridge error";
|
|
959
|
+
}
|
|
960
|
+
function normalizeRemoteHandlers(handlers) {
|
|
961
|
+
const out = /* @__PURE__ */ new Map();
|
|
962
|
+
for (const handler of handlers) {
|
|
963
|
+
if (handler.operation.length === 0) {
|
|
964
|
+
throw new RangeError("remoteResponder: operation must be non-empty");
|
|
965
|
+
}
|
|
966
|
+
if (out.has(handler.operation)) {
|
|
967
|
+
throw new RangeError(`remoteResponder: duplicate operation '${handler.operation}'`);
|
|
968
|
+
}
|
|
969
|
+
out.set(handler.operation, handler.handle);
|
|
970
|
+
}
|
|
971
|
+
return out;
|
|
972
|
+
}
|
|
973
|
+
function reduceRemoteResponderInbound(cursor, envelope, expectedSessionId) {
|
|
974
|
+
const envelopeError = validateInboundEnvelope(envelope);
|
|
975
|
+
if (envelopeError !== void 0) {
|
|
976
|
+
return { kind: "invalid", error: envelopeError };
|
|
977
|
+
}
|
|
978
|
+
if (envelope.sessionId !== expectedSessionId) {
|
|
979
|
+
return {
|
|
980
|
+
kind: "invalid",
|
|
981
|
+
error: `remoteResponder: inbound session ${envelope.sessionId} did not match expected ${expectedSessionId}`
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const seq = envelope.metadata.seq;
|
|
985
|
+
const expected = cursor.cursor + 1;
|
|
986
|
+
if (seq <= cursor.cursor) {
|
|
987
|
+
return {
|
|
988
|
+
kind: "invalid",
|
|
989
|
+
error: `remoteResponder: duplicate request seq ${seq} at cursor ${cursor.cursor}`
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
if (seq > expected) {
|
|
993
|
+
return {
|
|
994
|
+
kind: "invalid",
|
|
995
|
+
error: `remoteResponder: out-of-order request seq ${seq}, expected ${expected}`
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (envelope.metadata.cursor < cursor.remoteCursor) {
|
|
999
|
+
return {
|
|
1000
|
+
kind: "invalid",
|
|
1001
|
+
error: `${envelope.sessionId}: remoteResponder inbound cursor ${envelope.metadata.cursor} regressed below ${cursor.remoteCursor}`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
cursor.cursor = seq;
|
|
1005
|
+
cursor.remoteCursor = envelope.metadata.cursor;
|
|
1006
|
+
if (envelope.type !== "data") return { kind: "consumed" };
|
|
1007
|
+
if (envelope.payload?.kind !== "data") {
|
|
1008
|
+
return { kind: "invalid", error: "remoteResponder: request envelope must carry request DATA" };
|
|
1009
|
+
}
|
|
1010
|
+
const request = validateRemoteCallRequest(envelope.payload.value);
|
|
1011
|
+
if (request === void 0) {
|
|
1012
|
+
return { kind: "invalid", error: "remoteResponder: request payload is malformed" };
|
|
1013
|
+
}
|
|
1014
|
+
return { kind: "request", request, seq };
|
|
1015
|
+
}
|
|
1016
|
+
function remoteResponderEventsNode(graph, inbound, name, expectedSessionId, handlers, rejectUnknown) {
|
|
1017
|
+
return graph.node(
|
|
1018
|
+
[inbound],
|
|
1019
|
+
(ctx) => {
|
|
1020
|
+
const cursor = ctx.state.get() ?? { cursor: 0, remoteCursor: 0 };
|
|
1021
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1022
|
+
const envelope = raw;
|
|
1023
|
+
const received = reduceRemoteResponderInbound(cursor, envelope, expectedSessionId);
|
|
1024
|
+
if (received.kind === "consumed") continue;
|
|
1025
|
+
if (received.kind === "invalid") {
|
|
1026
|
+
ctx.down([["DATA", { kind: "invalid", error: received.error }]]);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const { request, seq } = received;
|
|
1030
|
+
ctx.down([["DATA", { kind: "request", request, seq }]]);
|
|
1031
|
+
const handler = handlers.get(request.operation);
|
|
1032
|
+
if (handler === void 0) {
|
|
1033
|
+
if (!rejectUnknown) continue;
|
|
1034
|
+
const error = `remoteResponder: unknown operation '${request.operation}'`;
|
|
1035
|
+
const response = remoteCallErrorResponse(request, error);
|
|
1036
|
+
ctx.down([
|
|
1037
|
+
[
|
|
1038
|
+
"DATA",
|
|
1039
|
+
{
|
|
1040
|
+
kind: "rejected",
|
|
1041
|
+
requestId: request.requestId,
|
|
1042
|
+
operation: request.operation,
|
|
1043
|
+
error,
|
|
1044
|
+
command: { kind: "send", payload: response, requestId: request.requestId }
|
|
1045
|
+
}
|
|
1046
|
+
]
|
|
1047
|
+
]);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const payload = handler(request);
|
|
1052
|
+
if (isThenable(payload)) {
|
|
1053
|
+
throw new Error("remoteResponder: async handler results require a later adapter shape");
|
|
1054
|
+
}
|
|
1055
|
+
const response = {
|
|
1056
|
+
kind: "result",
|
|
1057
|
+
operation: request.operation,
|
|
1058
|
+
requestId: request.requestId,
|
|
1059
|
+
payload
|
|
1060
|
+
};
|
|
1061
|
+
ctx.down([
|
|
1062
|
+
[
|
|
1063
|
+
"DATA",
|
|
1064
|
+
{
|
|
1065
|
+
kind: "response",
|
|
1066
|
+
requestId: request.requestId,
|
|
1067
|
+
operation: request.operation,
|
|
1068
|
+
command: { kind: "send", payload: response, requestId: request.requestId }
|
|
1069
|
+
}
|
|
1070
|
+
]
|
|
1071
|
+
]);
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
rethrowGraphRuntimeInvariant(error);
|
|
1074
|
+
const message = errorMessage(error);
|
|
1075
|
+
const response = remoteCallErrorResponse(request, message);
|
|
1076
|
+
ctx.down([
|
|
1077
|
+
[
|
|
1078
|
+
"DATA",
|
|
1079
|
+
{
|
|
1080
|
+
kind: "rejected",
|
|
1081
|
+
requestId: request.requestId,
|
|
1082
|
+
operation: request.operation,
|
|
1083
|
+
error: message,
|
|
1084
|
+
command: { kind: "send", payload: response, requestId: request.requestId }
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
]);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
ctx.state.set(cursor);
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: `${name}/events`,
|
|
1094
|
+
factory: "remoteResponderEvents",
|
|
1095
|
+
partial: true,
|
|
1096
|
+
completeWhenDepsComplete: false,
|
|
1097
|
+
errorWhenDepsError: false
|
|
1098
|
+
}
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
function remoteCallErrorResponse(request, error) {
|
|
1102
|
+
return {
|
|
1103
|
+
kind: "error",
|
|
1104
|
+
operation: request.operation,
|
|
1105
|
+
requestId: request.requestId,
|
|
1106
|
+
error
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function remoteResponderResponseCommandsNode(graph, events, name) {
|
|
1110
|
+
return graph.node(
|
|
1111
|
+
[events],
|
|
1112
|
+
(ctx) => {
|
|
1113
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1114
|
+
const event = raw;
|
|
1115
|
+
if ((event.kind === "response" || event.kind === "rejected") && event.command !== void 0) {
|
|
1116
|
+
ctx.down([["DATA", event.command]]);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: `${name}/responseCommands`,
|
|
1122
|
+
factory: "remoteResponderResponseCommands",
|
|
1123
|
+
completeWhenDepsComplete: false,
|
|
1124
|
+
errorWhenDepsError: false
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
function remoteResponderRequestsNode(graph, events, name) {
|
|
1129
|
+
return graph.node(
|
|
1130
|
+
[events],
|
|
1131
|
+
(ctx) => {
|
|
1132
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1133
|
+
const event = raw;
|
|
1134
|
+
if (event.kind === "request") ctx.down([["DATA", event.request]]);
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
name: `${name}/requests`,
|
|
1139
|
+
factory: "remoteResponderRequests",
|
|
1140
|
+
completeWhenDepsComplete: false
|
|
1141
|
+
}
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
function remoteResponderStatusNode(graph, events, name) {
|
|
1145
|
+
return graph.node(
|
|
1146
|
+
[events],
|
|
1147
|
+
(ctx) => {
|
|
1148
|
+
let status = ctx.state.get() ?? { state: "idle", handled: 0, rejected: 0, errors: 0 };
|
|
1149
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1150
|
+
const event = raw;
|
|
1151
|
+
if (event.kind === "response") {
|
|
1152
|
+
status = {
|
|
1153
|
+
...status,
|
|
1154
|
+
state: "responded",
|
|
1155
|
+
operation: event.operation,
|
|
1156
|
+
requestId: event.requestId,
|
|
1157
|
+
handled: status.handled + 1
|
|
1158
|
+
};
|
|
1159
|
+
} else if (event.kind === "rejected") {
|
|
1160
|
+
status = {
|
|
1161
|
+
...status,
|
|
1162
|
+
state: "rejected",
|
|
1163
|
+
operation: event.operation,
|
|
1164
|
+
requestId: event.requestId,
|
|
1165
|
+
rejected: status.rejected + 1,
|
|
1166
|
+
errors: status.errors + 1
|
|
1167
|
+
};
|
|
1168
|
+
} else if (event.kind === "invalid") {
|
|
1169
|
+
status = { ...status, state: "errored", errors: status.errors + 1 };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
ctx.state.set(status);
|
|
1173
|
+
ctx.down([["DATA", status]]);
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
name: `${name}/status`,
|
|
1177
|
+
factory: "remoteResponderStatus",
|
|
1178
|
+
partial: true,
|
|
1179
|
+
completeWhenDepsComplete: false,
|
|
1180
|
+
errorWhenDepsError: false
|
|
1181
|
+
}
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
function remoteResponderErrorsNode(graph, events, name) {
|
|
1185
|
+
return graph.node(
|
|
1186
|
+
[events],
|
|
1187
|
+
(ctx) => {
|
|
1188
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1189
|
+
const event = raw;
|
|
1190
|
+
if (event.kind === "rejected") {
|
|
1191
|
+
ctx.down([
|
|
1192
|
+
[
|
|
1193
|
+
"DATA",
|
|
1194
|
+
{ operation: event.operation, requestId: event.requestId, error: event.error }
|
|
1195
|
+
]
|
|
1196
|
+
]);
|
|
1197
|
+
} else if (event.kind === "invalid") {
|
|
1198
|
+
ctx.down([["DATA", { error: event.error }]]);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
{ name: `${name}/errors`, factory: "remoteResponderErrors", completeWhenDepsComplete: false }
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
function armAckTimeout(ctx, state, envelope, opts, policy, now) {
|
|
1206
|
+
const timeoutMs = opts.ackTimeoutMs ?? defaultAckTimeoutMs;
|
|
1207
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
1208
|
+
throw new RangeError("wireBridge: ackTimeoutMs must be a non-negative finite number");
|
|
1209
|
+
}
|
|
1210
|
+
const pending = { envelope };
|
|
1211
|
+
const schedule = (current) => {
|
|
1212
|
+
pending.timer = setTimeout(() => {
|
|
1213
|
+
pending.timer = void 0;
|
|
1214
|
+
if (!state.active || !state.pending.has(current.metadata.seq)) return;
|
|
1215
|
+
ctx.down([
|
|
1216
|
+
[
|
|
1217
|
+
"DATA",
|
|
1218
|
+
{
|
|
1219
|
+
kind: "timeout",
|
|
1220
|
+
seq: current.metadata.seq,
|
|
1221
|
+
attempt: current.metadata.attempt
|
|
1222
|
+
}
|
|
1223
|
+
]
|
|
1224
|
+
]);
|
|
1225
|
+
if (!shouldRetry(policy, current.metadata.attempt)) {
|
|
1226
|
+
const error = `${opts.sessionId}: ack timeout for seq ${current.metadata.seq}`;
|
|
1227
|
+
state.pending.delete(current.metadata.seq);
|
|
1228
|
+
ctx.down([
|
|
1229
|
+
[
|
|
1230
|
+
"DATA",
|
|
1231
|
+
{
|
|
1232
|
+
kind: "exhausted",
|
|
1233
|
+
seq: current.metadata.seq,
|
|
1234
|
+
attempt: current.metadata.attempt,
|
|
1235
|
+
error
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
]);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const attempt = current.metadata.attempt + 1;
|
|
1242
|
+
const delayMs = nextRetryDelayMs(policy, attempt) ?? 0;
|
|
1243
|
+
ctx.down([
|
|
1244
|
+
[
|
|
1245
|
+
"DATA",
|
|
1246
|
+
{
|
|
1247
|
+
kind: "retry",
|
|
1248
|
+
seq: current.metadata.seq,
|
|
1249
|
+
attempt,
|
|
1250
|
+
delayMs,
|
|
1251
|
+
error: `${opts.sessionId}: ack timeout for seq ${current.metadata.seq}`
|
|
1252
|
+
}
|
|
1253
|
+
]
|
|
1254
|
+
]);
|
|
1255
|
+
const retry = wireBridgeEnvelope({
|
|
1256
|
+
sessionId: current.sessionId,
|
|
1257
|
+
type: current.type,
|
|
1258
|
+
seq: current.metadata.seq,
|
|
1259
|
+
cursor: state.cursor,
|
|
1260
|
+
payload: current.payload,
|
|
1261
|
+
idempotencyKey: current.metadata.idempotencyKey,
|
|
1262
|
+
attempt,
|
|
1263
|
+
maxAttempts: policy.maxAttempts,
|
|
1264
|
+
timestampMs: now(),
|
|
1265
|
+
requestId: current.metadata.requestId
|
|
1266
|
+
});
|
|
1267
|
+
pending.timer = setTimeout(() => {
|
|
1268
|
+
pending.timer = void 0;
|
|
1269
|
+
if (!state.active || !state.pending.has(current.metadata.seq)) return;
|
|
1270
|
+
pending.envelope = retry;
|
|
1271
|
+
ctx.down([["DATA", { kind: "outbound", envelope: retry }]]);
|
|
1272
|
+
schedule(retry);
|
|
1273
|
+
}, delayMs);
|
|
1274
|
+
}, timeoutMs);
|
|
1275
|
+
};
|
|
1276
|
+
state.pending.set(envelope.metadata.seq, pending);
|
|
1277
|
+
schedule(envelope);
|
|
1278
|
+
}
|
|
1279
|
+
function processInbound(ctx, state, input, sessionId) {
|
|
1280
|
+
if (isInvalidIngress(input)) {
|
|
1281
|
+
ctx.down([["DATA", { kind: "invalid", error: input.error }]]);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const invalid = validateInboundEnvelope(input);
|
|
1285
|
+
if (invalid !== void 0) {
|
|
1286
|
+
ctx.down([["DATA", { kind: "invalid", error: invalid }]]);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const envelope = input;
|
|
1290
|
+
if (envelope.sessionId !== sessionId) {
|
|
1291
|
+
ctx.down([
|
|
1292
|
+
[
|
|
1293
|
+
"DATA",
|
|
1294
|
+
{
|
|
1295
|
+
kind: "session-mismatch",
|
|
1296
|
+
expected: sessionId,
|
|
1297
|
+
actual: envelope.sessionId
|
|
1298
|
+
}
|
|
1299
|
+
]
|
|
1300
|
+
]);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
const seq = envelope.metadata.seq;
|
|
1304
|
+
const expected = state.cursor + 1;
|
|
1305
|
+
if (seq <= state.cursor) {
|
|
1306
|
+
ctx.down([["DATA", { kind: "duplicate", seq, cursor: state.cursor }]]);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (seq > expected) {
|
|
1310
|
+
ctx.down([["DATA", { kind: "out-of-order", seq, expected }]]);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (envelope.metadata.cursor < state.remoteCursor) {
|
|
1314
|
+
ctx.down([
|
|
1315
|
+
[
|
|
1316
|
+
"DATA",
|
|
1317
|
+
{
|
|
1318
|
+
kind: "invalid",
|
|
1319
|
+
error: `${sessionId}: inbound cursor ${envelope.metadata.cursor} regressed below ${state.remoteCursor}`
|
|
1320
|
+
}
|
|
1321
|
+
]
|
|
1322
|
+
]);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
state.remoteCursor = envelope.metadata.cursor;
|
|
1326
|
+
state.cursor = seq;
|
|
1327
|
+
ctx.down([["DATA", { kind: "cursor", cursor: state.cursor }]]);
|
|
1328
|
+
ctx.down([["DATA", { kind: "inbound", envelope }]]);
|
|
1329
|
+
if (envelope.type === "ack" && envelope.metadata.ackForSeq !== void 0) {
|
|
1330
|
+
const pending = state.pending.get(envelope.metadata.ackForSeq);
|
|
1331
|
+
if (pending === void 0) {
|
|
1332
|
+
ctx.down([
|
|
1333
|
+
["DATA", { kind: "late-receipt", receipt: "ack", ackForSeq: envelope.metadata.ackForSeq }]
|
|
1334
|
+
]);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (pending?.timer !== void 0) clearTimeout(pending.timer);
|
|
1338
|
+
state.pending.delete(envelope.metadata.ackForSeq);
|
|
1339
|
+
ctx.down([
|
|
1340
|
+
[
|
|
1341
|
+
"DATA",
|
|
1342
|
+
{
|
|
1343
|
+
kind: "ack",
|
|
1344
|
+
ackForSeq: envelope.metadata.ackForSeq,
|
|
1345
|
+
envelope,
|
|
1346
|
+
outbound: pending.envelope
|
|
1347
|
+
}
|
|
1348
|
+
]
|
|
1349
|
+
]);
|
|
1350
|
+
} else if (envelope.type === "nack" && envelope.metadata.ackForSeq !== void 0) {
|
|
1351
|
+
const pending = state.pending.get(envelope.metadata.ackForSeq);
|
|
1352
|
+
if (pending === void 0) {
|
|
1353
|
+
ctx.down([
|
|
1354
|
+
["DATA", { kind: "late-receipt", receipt: "nack", ackForSeq: envelope.metadata.ackForSeq }]
|
|
1355
|
+
]);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (pending?.timer !== void 0) clearTimeout(pending.timer);
|
|
1359
|
+
state.pending.delete(envelope.metadata.ackForSeq);
|
|
1360
|
+
ctx.down([
|
|
1361
|
+
[
|
|
1362
|
+
"DATA",
|
|
1363
|
+
{
|
|
1364
|
+
kind: "nack",
|
|
1365
|
+
ackForSeq: envelope.metadata.ackForSeq,
|
|
1366
|
+
envelope,
|
|
1367
|
+
outbound: pending.envelope,
|
|
1368
|
+
error: bridgePayloadError(envelope.payload, "remote nack")
|
|
1369
|
+
}
|
|
1370
|
+
]
|
|
1371
|
+
]);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function validateInboundEnvelope(envelope) {
|
|
1375
|
+
if (typeof envelope !== "object" || envelope === null) {
|
|
1376
|
+
return "wireBridge: inbound envelope must be an object";
|
|
1377
|
+
}
|
|
1378
|
+
const candidate = envelope;
|
|
1379
|
+
if (typeof candidate.sessionId !== "string" || candidate.sessionId.length === 0) {
|
|
1380
|
+
return "wireBridge: inbound envelope sessionId must be a non-empty string";
|
|
1381
|
+
}
|
|
1382
|
+
if (typeof candidate.type !== "string" || !envelopeTypes.has(candidate.type)) {
|
|
1383
|
+
return "wireBridge: inbound envelope type is not recognized";
|
|
1384
|
+
}
|
|
1385
|
+
const metadata = candidate.metadata;
|
|
1386
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
1387
|
+
return "wireBridge: inbound envelope metadata must be an object";
|
|
1388
|
+
}
|
|
1389
|
+
if (!isSafePositiveInteger(metadata.seq)) {
|
|
1390
|
+
return "wireBridge: inbound envelope seq must be a positive integer";
|
|
1391
|
+
}
|
|
1392
|
+
if (!isSafeNonNegativeInteger(metadata.cursor)) {
|
|
1393
|
+
return "wireBridge: inbound envelope cursor must be a non-negative integer";
|
|
1394
|
+
}
|
|
1395
|
+
if (typeof metadata.idempotencyKey !== "string" || metadata.idempotencyKey.length === 0) {
|
|
1396
|
+
return "wireBridge: inbound envelope idempotencyKey must be a non-empty string";
|
|
1397
|
+
}
|
|
1398
|
+
if (!isSafePositiveInteger(metadata.attempt)) {
|
|
1399
|
+
return "wireBridge: inbound envelope attempt must be a positive integer";
|
|
1400
|
+
}
|
|
1401
|
+
if (!isSafePositiveInteger(metadata.maxAttempts) || metadata.maxAttempts < metadata.attempt) {
|
|
1402
|
+
return "wireBridge: inbound envelope maxAttempts must be >= attempt";
|
|
1403
|
+
}
|
|
1404
|
+
if (metadata.ackForSeq !== void 0 && !isSafePositiveInteger(metadata.ackForSeq)) {
|
|
1405
|
+
return "wireBridge: inbound envelope ackForSeq must be a positive integer";
|
|
1406
|
+
}
|
|
1407
|
+
if ((candidate.type === "ack" || candidate.type === "nack") && metadata.ackForSeq === void 0) {
|
|
1408
|
+
return `wireBridge: inbound ${candidate.type} envelope requires ackForSeq`;
|
|
1409
|
+
}
|
|
1410
|
+
const payloadError = validatePayloadForType(
|
|
1411
|
+
candidate.type,
|
|
1412
|
+
candidate.payload,
|
|
1413
|
+
"wireBridge: inbound envelope"
|
|
1414
|
+
);
|
|
1415
|
+
if (payloadError !== void 0) return payloadError;
|
|
1416
|
+
return void 0;
|
|
1417
|
+
}
|
|
1418
|
+
function validateCommand(value) {
|
|
1419
|
+
if (typeof value !== "object" || value === null) {
|
|
1420
|
+
return "wireBridge: command fact must be an object";
|
|
1421
|
+
}
|
|
1422
|
+
const kind = value.kind;
|
|
1423
|
+
if (kind !== "start" && kind !== "send" && kind !== "ack" && kind !== "nack" && kind !== "close") {
|
|
1424
|
+
return "wireBridge: command kind is not recognized";
|
|
1425
|
+
}
|
|
1426
|
+
if ((kind === "ack" || kind === "nack") && !isSafePositiveInteger(value.ackForSeq)) {
|
|
1427
|
+
return `wireBridge: ${kind} command ackForSeq must be a positive integer`;
|
|
1428
|
+
}
|
|
1429
|
+
return void 0;
|
|
1430
|
+
}
|
|
1431
|
+
function projectOutbound(graph, events, name) {
|
|
1432
|
+
return graph.node(
|
|
1433
|
+
[events],
|
|
1434
|
+
(ctx) => {
|
|
1435
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1436
|
+
const event = raw;
|
|
1437
|
+
if (event.kind === "outbound") ctx.down([["DATA", event.envelope]]);
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
{ name: `${name}/outbound`, factory: "wireBridgeOutbound" }
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
function projectAcks(graph, events, name) {
|
|
1444
|
+
return graph.node(
|
|
1445
|
+
[events],
|
|
1446
|
+
(ctx) => {
|
|
1447
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1448
|
+
const event = raw;
|
|
1449
|
+
if (event.kind === "ack") {
|
|
1450
|
+
ctx.down([["DATA", { ackForSeq: event.ackForSeq, envelope: event.envelope }]]);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
{ name: `${name}/acks`, factory: "wireBridgeAcks" }
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
function projectNacks(graph, events, name) {
|
|
1458
|
+
return graph.node(
|
|
1459
|
+
[events],
|
|
1460
|
+
(ctx) => {
|
|
1461
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1462
|
+
const event = raw;
|
|
1463
|
+
if (event.kind === "nack") {
|
|
1464
|
+
ctx.down([
|
|
1465
|
+
["DATA", { ackForSeq: event.ackForSeq, envelope: event.envelope, error: event.error }]
|
|
1466
|
+
]);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
},
|
|
1470
|
+
{ name: `${name}/nacks`, factory: "wireBridgeNacks" }
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
function projectStatus(graph, events, name, sessionId) {
|
|
1474
|
+
return graph.node(
|
|
1475
|
+
[events],
|
|
1476
|
+
(ctx) => {
|
|
1477
|
+
let next = ctx.state.get() ?? {
|
|
1478
|
+
sessionId,
|
|
1479
|
+
state: "idle",
|
|
1480
|
+
cursor: 0,
|
|
1481
|
+
nextSeq: 1,
|
|
1482
|
+
pending: 0,
|
|
1483
|
+
attempts: 0,
|
|
1484
|
+
acked: 0,
|
|
1485
|
+
nacked: 0,
|
|
1486
|
+
errors: 0
|
|
1487
|
+
};
|
|
1488
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1489
|
+
const event = raw;
|
|
1490
|
+
if (event.kind === "outbound") {
|
|
1491
|
+
const { seq, attempt } = event.envelope.metadata;
|
|
1492
|
+
const trackAck = shouldTrackAck(event.envelope.type);
|
|
1493
|
+
next = {
|
|
1494
|
+
...next,
|
|
1495
|
+
state: event.envelope.type === "start" ? "started" : event.envelope.type === "close" ? "closed" : "open",
|
|
1496
|
+
nextSeq: Math.max(next.nextSeq, seq + 1),
|
|
1497
|
+
pending: event.envelope.type === "close" ? 1 : trackAck && attempt === 1 ? next.pending + 1 : next.pending,
|
|
1498
|
+
attempts: trackAck ? next.attempts + 1 : next.attempts,
|
|
1499
|
+
lastSeq: seq
|
|
1500
|
+
};
|
|
1501
|
+
} else if (event.kind === "ack") {
|
|
1502
|
+
next = {
|
|
1503
|
+
...next,
|
|
1504
|
+
state: event.outbound.type === "close" ? "closed" : "open",
|
|
1505
|
+
pending: Math.max(0, next.pending - 1),
|
|
1506
|
+
acked: next.acked + 1,
|
|
1507
|
+
lastSeq: event.envelope.metadata.seq
|
|
1508
|
+
};
|
|
1509
|
+
} else if (event.kind === "nack") {
|
|
1510
|
+
next = {
|
|
1511
|
+
...next,
|
|
1512
|
+
state: "errored",
|
|
1513
|
+
pending: Math.max(0, next.pending - 1),
|
|
1514
|
+
nacked: next.nacked + 1,
|
|
1515
|
+
errors: next.errors + 1,
|
|
1516
|
+
lastSeq: event.envelope.metadata.seq
|
|
1517
|
+
};
|
|
1518
|
+
} else if (event.kind === "retry") {
|
|
1519
|
+
next = {
|
|
1520
|
+
...next,
|
|
1521
|
+
state: "waiting",
|
|
1522
|
+
lastSeq: event.seq,
|
|
1523
|
+
lastDelayMs: event.delayMs
|
|
1524
|
+
};
|
|
1525
|
+
} else if (event.kind === "exhausted") {
|
|
1526
|
+
next = {
|
|
1527
|
+
...next,
|
|
1528
|
+
state: "exhausted",
|
|
1529
|
+
pending: Math.max(0, next.pending - 1),
|
|
1530
|
+
errors: next.errors + 1,
|
|
1531
|
+
lastSeq: event.seq
|
|
1532
|
+
};
|
|
1533
|
+
} else if (event.kind === "cursor") {
|
|
1534
|
+
next = { ...next, cursor: event.cursor };
|
|
1535
|
+
} else if (event.kind === "out-of-order") {
|
|
1536
|
+
next = { ...next, state: "errored", errors: next.errors + 1, lastSeq: event.seq };
|
|
1537
|
+
} else if (event.kind === "session-mismatch") {
|
|
1538
|
+
next = { ...next, state: "errored", errors: next.errors + 1 };
|
|
1539
|
+
} else if (event.kind === "late-receipt" || event.kind === "invalid") {
|
|
1540
|
+
next = { ...next, state: "errored", errors: next.errors + 1 };
|
|
1541
|
+
} else if (event.kind === "inbound" && event.envelope.type === "error") {
|
|
1542
|
+
next = {
|
|
1543
|
+
...next,
|
|
1544
|
+
state: "errored",
|
|
1545
|
+
errors: next.errors + 1,
|
|
1546
|
+
lastSeq: event.envelope.metadata.seq
|
|
1547
|
+
};
|
|
1548
|
+
} else if (event.kind === "inbound" && event.envelope.type === "close") {
|
|
1549
|
+
next = { ...next, state: "closed", lastSeq: event.envelope.metadata.seq };
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
ctx.state.set(next);
|
|
1553
|
+
ctx.down([["DATA", next]]);
|
|
1554
|
+
},
|
|
1555
|
+
{ name: `${name}/status`, factory: "wireBridgeStatus" }
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
function projectErrors(graph, events, name) {
|
|
1559
|
+
return graph.node(
|
|
1560
|
+
[events],
|
|
1561
|
+
(ctx) => {
|
|
1562
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1563
|
+
const event = raw;
|
|
1564
|
+
if (event.kind === "nack") ctx.down([["DATA", event.error]]);
|
|
1565
|
+
else if (event.kind === "exhausted") ctx.down([["DATA", event.error]]);
|
|
1566
|
+
else if (event.kind === "out-of-order") {
|
|
1567
|
+
ctx.down([
|
|
1568
|
+
[
|
|
1569
|
+
"DATA",
|
|
1570
|
+
`${name}: inbound seq ${event.seq} arrived before expected seq ${event.expected}`
|
|
1571
|
+
]
|
|
1572
|
+
]);
|
|
1573
|
+
} else if (event.kind === "inbound" && event.envelope.type === "error") {
|
|
1574
|
+
ctx.down([["DATA", bridgePayloadError(event.envelope.payload, "remote error envelope")]]);
|
|
1575
|
+
} else if (event.kind === "session-mismatch") {
|
|
1576
|
+
ctx.down([
|
|
1577
|
+
[
|
|
1578
|
+
"DATA",
|
|
1579
|
+
`${name}: inbound session ${event.actual} did not match expected ${event.expected}`
|
|
1580
|
+
]
|
|
1581
|
+
]);
|
|
1582
|
+
} else if (event.kind === "late-receipt") {
|
|
1583
|
+
ctx.down([
|
|
1584
|
+
[
|
|
1585
|
+
"DATA",
|
|
1586
|
+
`${name}: late ${event.receipt} for unknown or completed ackForSeq ${event.ackForSeq}`
|
|
1587
|
+
]
|
|
1588
|
+
]);
|
|
1589
|
+
} else if (event.kind === "invalid") {
|
|
1590
|
+
ctx.down([["DATA", event.error]]);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
{ name: `${name}/errors`, factory: "wireBridgeErrors" }
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
function projectCursor(graph, events, name) {
|
|
1598
|
+
return graph.node(
|
|
1599
|
+
[events],
|
|
1600
|
+
(ctx) => {
|
|
1601
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1602
|
+
const event = raw;
|
|
1603
|
+
if (event.kind === "cursor") ctx.down([["DATA", event.cursor]]);
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
{ name: `${name}/cursor`, factory: "wireBridgeCursor" }
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
function projectAttempts(graph, events, name) {
|
|
1610
|
+
return graph.node(
|
|
1611
|
+
[events],
|
|
1612
|
+
(ctx) => {
|
|
1613
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
1614
|
+
const event = raw;
|
|
1615
|
+
if (event.kind === "outbound" && shouldTrackAck(event.envelope.type)) {
|
|
1616
|
+
ctx.down([
|
|
1617
|
+
[
|
|
1618
|
+
"DATA",
|
|
1619
|
+
{
|
|
1620
|
+
seq: event.envelope.metadata.seq,
|
|
1621
|
+
attempt: event.envelope.metadata.attempt,
|
|
1622
|
+
maxAttempts: event.envelope.metadata.maxAttempts
|
|
1623
|
+
}
|
|
1624
|
+
]
|
|
1625
|
+
]);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
{ name: `${name}/attempts`, factory: "wireBridgeAttempts" }
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
function guardedInboundNode(node, sessionId) {
|
|
1633
|
+
return new Proxy(node, {
|
|
1634
|
+
get(target, property) {
|
|
1635
|
+
if (property === "down") {
|
|
1636
|
+
return (msgs) => target.down(guardInboundWave(msgs, sessionId));
|
|
1637
|
+
}
|
|
1638
|
+
const value = Reflect.get(target, property, target);
|
|
1639
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
function guardInboundWave(msgs, sessionId) {
|
|
1644
|
+
return msgs.map((msg) => {
|
|
1645
|
+
if (msg[0] === "DATA") return msg;
|
|
1646
|
+
if (msg[0] === "ERROR") {
|
|
1647
|
+
return [
|
|
1648
|
+
"DATA",
|
|
1649
|
+
invalidIngress(
|
|
1650
|
+
`${sessionId}: inbound protocol ERROR ${String(
|
|
1651
|
+
errorPayload(msg[1])
|
|
1652
|
+
)} is local misuse; remote errors must arrive as DATA envelope facts`
|
|
1653
|
+
)
|
|
1654
|
+
];
|
|
1655
|
+
}
|
|
1656
|
+
if (msg[0] === "COMPLETE") {
|
|
1657
|
+
return [
|
|
1658
|
+
"DATA",
|
|
1659
|
+
invalidIngress(
|
|
1660
|
+
`${sessionId}: inbound protocol COMPLETE is local misuse; remote completion must arrive as a DATA envelope fact`
|
|
1661
|
+
)
|
|
1662
|
+
];
|
|
1663
|
+
}
|
|
1664
|
+
return [
|
|
1665
|
+
"DATA",
|
|
1666
|
+
invalidIngress(
|
|
1667
|
+
`${sessionId}: inbound port accepts DATA envelope facts only; ${msg[0]} is local protocol traffic`
|
|
1668
|
+
)
|
|
1669
|
+
];
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
function invalidIngress(error) {
|
|
1673
|
+
return { __wireBridgeInvalidIngress: true, error };
|
|
1674
|
+
}
|
|
1675
|
+
function isInvalidIngress(value) {
|
|
1676
|
+
return typeof value === "object" && value !== null && value.__wireBridgeInvalidIngress === true && typeof value.error === "string";
|
|
1677
|
+
}
|
|
1678
|
+
function shouldTrackAck(type) {
|
|
1679
|
+
return type === "start" || type === "data" || type === "close";
|
|
1680
|
+
}
|
|
1681
|
+
function isSafePositiveInteger(value) {
|
|
1682
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1683
|
+
}
|
|
1684
|
+
function isSafeNonNegativeInteger(value) {
|
|
1685
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
1686
|
+
}
|
|
1687
|
+
function validatePayloadForType(type, payload, prefix) {
|
|
1688
|
+
const kind = typeof payload === "object" && payload !== null ? payload.kind : void 0;
|
|
1689
|
+
switch (type) {
|
|
1690
|
+
case "data":
|
|
1691
|
+
return kind === "data" && payload.value !== void 0 ? void 0 : `${prefix}: data envelope requires data payload`;
|
|
1692
|
+
case "nack":
|
|
1693
|
+
case "error":
|
|
1694
|
+
return kind === "error" && payload.error !== void 0 ? void 0 : `${prefix}: ${type} envelope requires error payload`;
|
|
1695
|
+
case "status":
|
|
1696
|
+
return kind === "status" && payload.status !== void 0 ? void 0 : `${prefix}: status envelope requires status payload`;
|
|
1697
|
+
case "close":
|
|
1698
|
+
return kind === "close" ? void 0 : `${prefix}: close envelope requires close payload`;
|
|
1699
|
+
case "start":
|
|
1700
|
+
case "ack":
|
|
1701
|
+
return payload === void 0 ? void 0 : `${prefix}: ${type} envelope must not carry a payload`;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function bridgePayloadError(payload, fallback) {
|
|
1705
|
+
if (typeof payload === "object" && payload !== null && payload.kind === "error") {
|
|
1706
|
+
return payload.error;
|
|
1707
|
+
}
|
|
1708
|
+
return fallback;
|
|
1709
|
+
}
|
|
1710
|
+
function rethrowGraphRuntimeInvariant(error) {
|
|
1711
|
+
const message = errorMessage(error);
|
|
1712
|
+
if (message.includes("R-reentrancy") || message.includes("R-rewire") || message.includes("R-graph-domain") || message.includes("D37") || message.includes("D22") || message.includes("different graph") || message.includes("cross-graph") || message.includes("wire bridge") || message.includes("mid-fn topology mutation") || message.includes("reentrant dep mutation") || message.includes("feedback cycle")) {
|
|
1713
|
+
throw error;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function errorMessage(error) {
|
|
1717
|
+
return error instanceof Error ? error.message : String(error);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/adapters/environment.ts
|
|
1721
|
+
function assertSourceLocal(source) {
|
|
1722
|
+
return source;
|
|
1723
|
+
}
|
|
1724
|
+
function initSendState(ctx) {
|
|
1725
|
+
let state = ctx.state.get();
|
|
1726
|
+
if (state === void 0) {
|
|
1727
|
+
state = {
|
|
1728
|
+
seq: 0,
|
|
1729
|
+
cancels: /* @__PURE__ */ new Map(),
|
|
1730
|
+
timers: /* @__PURE__ */ new Map(),
|
|
1731
|
+
active: true,
|
|
1732
|
+
cleanupInstalled: false
|
|
1733
|
+
};
|
|
1734
|
+
ctx.state.set(state);
|
|
1735
|
+
}
|
|
1736
|
+
state.active = true;
|
|
1737
|
+
if (!state.cleanupInstalled) {
|
|
1738
|
+
state.cleanupInstalled = true;
|
|
1739
|
+
ctx.onDeactivation(() => {
|
|
1740
|
+
state.active = false;
|
|
1741
|
+
state.cleanupInstalled = false;
|
|
1742
|
+
for (const cancel of state.cancels.values()) runCancel(cancel);
|
|
1743
|
+
for (const timer of state.timers.values()) clearTimeout(timer);
|
|
1744
|
+
state.cancels.clear();
|
|
1745
|
+
state.timers.clear();
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
return state;
|
|
1749
|
+
}
|
|
1750
|
+
function runCancel(cancel) {
|
|
1751
|
+
try {
|
|
1752
|
+
cancel();
|
|
1753
|
+
} catch {
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
function outboundNodes(graph, events, name) {
|
|
1757
|
+
const status = graph.node(
|
|
1758
|
+
[events],
|
|
1759
|
+
(ctx) => {
|
|
1760
|
+
const current = ctx.state.get() ?? { state: "idle", inFlight: 0, attempt: 0, sent: 0, failed: 0 };
|
|
1761
|
+
let next = current;
|
|
1762
|
+
for (const event of depBatch(ctx, 0) ?? []) {
|
|
1763
|
+
next = reduceStatus(next, event);
|
|
1764
|
+
}
|
|
1765
|
+
ctx.state.set(next);
|
|
1766
|
+
ctx.down([["DATA", next]]);
|
|
1767
|
+
},
|
|
1768
|
+
{ name: `${name}/status`, factory: "outboundStatus" }
|
|
1769
|
+
);
|
|
1770
|
+
const attempts = graph.node(
|
|
1771
|
+
[events],
|
|
1772
|
+
(ctx) => {
|
|
1773
|
+
for (const event of depBatch(ctx, 0) ?? []) {
|
|
1774
|
+
const typed = event;
|
|
1775
|
+
if ("attempt" in typed) ctx.down([["DATA", typed.attempt]]);
|
|
1776
|
+
}
|
|
1777
|
+
},
|
|
1778
|
+
{ name: `${name}/attempts`, factory: "outboundAttempts" }
|
|
1779
|
+
);
|
|
1780
|
+
const errors = graph.node(
|
|
1781
|
+
[events],
|
|
1782
|
+
(ctx) => {
|
|
1783
|
+
for (const event of depBatch(ctx, 0) ?? []) {
|
|
1784
|
+
const typed = event;
|
|
1785
|
+
if ("error" in typed) ctx.down([["DATA", typed.error]]);
|
|
1786
|
+
}
|
|
1787
|
+
},
|
|
1788
|
+
{ name: `${name}/errors`, factory: "outboundErrors" }
|
|
1789
|
+
);
|
|
1790
|
+
return { events, status, attempts, errors };
|
|
1791
|
+
}
|
|
1792
|
+
function reduceStatus(current, event) {
|
|
1793
|
+
switch (event.kind) {
|
|
1794
|
+
case "attempt":
|
|
1795
|
+
return {
|
|
1796
|
+
...current,
|
|
1797
|
+
state: "running",
|
|
1798
|
+
inFlight: current.inFlight + 1,
|
|
1799
|
+
attempt: event.attempt
|
|
1800
|
+
};
|
|
1801
|
+
case "retry":
|
|
1802
|
+
return {
|
|
1803
|
+
...current,
|
|
1804
|
+
state: "waiting",
|
|
1805
|
+
inFlight: Math.max(0, current.inFlight - 1),
|
|
1806
|
+
attempt: event.attempt,
|
|
1807
|
+
lastDelayMs: event.delayMs
|
|
1808
|
+
};
|
|
1809
|
+
case "sent":
|
|
1810
|
+
return {
|
|
1811
|
+
...current,
|
|
1812
|
+
state: "succeeded",
|
|
1813
|
+
inFlight: Math.max(0, current.inFlight - 1),
|
|
1814
|
+
attempt: event.attempt,
|
|
1815
|
+
sent: current.sent + 1,
|
|
1816
|
+
lastDelayMs: void 0
|
|
1817
|
+
};
|
|
1818
|
+
case "failed":
|
|
1819
|
+
return {
|
|
1820
|
+
...current,
|
|
1821
|
+
state: "failed",
|
|
1822
|
+
inFlight: Math.max(0, current.inFlight - 1),
|
|
1823
|
+
attempt: event.attempt,
|
|
1824
|
+
failed: current.failed + 1
|
|
1825
|
+
};
|
|
1826
|
+
case "exhausted":
|
|
1827
|
+
return {
|
|
1828
|
+
...current,
|
|
1829
|
+
state: "exhausted",
|
|
1830
|
+
inFlight: Math.max(0, current.inFlight - 1),
|
|
1831
|
+
attempt: event.attempt,
|
|
1832
|
+
failed: current.failed + 1
|
|
1833
|
+
};
|
|
1834
|
+
case "upstream-complete":
|
|
1835
|
+
return { ...current, state: "completed" };
|
|
1836
|
+
case "upstream-error":
|
|
1837
|
+
return { ...current, state: "failed", failed: current.failed + 1 };
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function initWebSocketSessionState(ctx) {
|
|
1841
|
+
let state = ctx.state.get();
|
|
1842
|
+
if (state === void 0) {
|
|
1843
|
+
state = {
|
|
1844
|
+
active: true,
|
|
1845
|
+
connected: false,
|
|
1846
|
+
cleanupInstalled: false,
|
|
1847
|
+
currentAttempt: 0,
|
|
1848
|
+
currentSession: void 0,
|
|
1849
|
+
sendCancels: /* @__PURE__ */ new Map(),
|
|
1850
|
+
liveSends: /* @__PURE__ */ new Map(),
|
|
1851
|
+
pendingOutbound: [],
|
|
1852
|
+
timers: /* @__PURE__ */ new Map(),
|
|
1853
|
+
seq: 0,
|
|
1854
|
+
outboundSeq: 0
|
|
1855
|
+
};
|
|
1856
|
+
ctx.state.set(state);
|
|
1857
|
+
}
|
|
1858
|
+
state.active = true;
|
|
1859
|
+
if (!state.cleanupInstalled) {
|
|
1860
|
+
state.cleanupInstalled = true;
|
|
1861
|
+
ctx.onDeactivation(() => {
|
|
1862
|
+
const sendCancels = [...state.sendCancels.values()];
|
|
1863
|
+
const timers = [...state.timers.values()];
|
|
1864
|
+
const session = state.currentSession;
|
|
1865
|
+
state.active = false;
|
|
1866
|
+
state.cleanupInstalled = false;
|
|
1867
|
+
state.connected = false;
|
|
1868
|
+
state.currentAttempt = 0;
|
|
1869
|
+
state.currentSession = void 0;
|
|
1870
|
+
state.sendCancels.clear();
|
|
1871
|
+
state.liveSends.clear();
|
|
1872
|
+
state.pendingOutbound = [];
|
|
1873
|
+
state.timers.clear();
|
|
1874
|
+
for (const cancel of sendCancels) runCancel(cancel);
|
|
1875
|
+
for (const timer of timers) clearTimeout(timer);
|
|
1876
|
+
session?.cancel();
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
return state;
|
|
1880
|
+
}
|
|
1881
|
+
function closeWebSocketSessionState(state, driverAction = "cancel") {
|
|
1882
|
+
const session = state.currentSession;
|
|
1883
|
+
const timers = [...state.timers.values()];
|
|
1884
|
+
const sendCancels = [...state.sendCancels.values()];
|
|
1885
|
+
const canceled = [...state.pendingOutbound, ...state.liveSends.values()];
|
|
1886
|
+
state.connected = false;
|
|
1887
|
+
state.currentAttempt = 0;
|
|
1888
|
+
state.currentSession = void 0;
|
|
1889
|
+
state.timers.clear();
|
|
1890
|
+
state.sendCancels.clear();
|
|
1891
|
+
state.pendingOutbound = [];
|
|
1892
|
+
state.liveSends.clear();
|
|
1893
|
+
for (const timer of timers) clearTimeout(timer);
|
|
1894
|
+
for (const cancelSend of sendCancels) runCancel(cancelSend);
|
|
1895
|
+
if (driverAction === "cancel") session?.cancel();
|
|
1896
|
+
return { canceled, session };
|
|
1897
|
+
}
|
|
1898
|
+
function cancelRetryTimers(state) {
|
|
1899
|
+
for (const timer of state.timers.values()) clearTimeout(timer);
|
|
1900
|
+
state.timers.clear();
|
|
1901
|
+
}
|
|
1902
|
+
function cleanupWebSocketConnection(state, driverAction = "cancel") {
|
|
1903
|
+
const session = state.currentSession;
|
|
1904
|
+
const sendCancels = [...state.sendCancels.values()];
|
|
1905
|
+
const canceled = [...state.liveSends.values()];
|
|
1906
|
+
state.connected = false;
|
|
1907
|
+
state.currentAttempt = 0;
|
|
1908
|
+
state.currentSession = void 0;
|
|
1909
|
+
state.sendCancels.clear();
|
|
1910
|
+
state.liveSends.clear();
|
|
1911
|
+
for (const cancelSend of sendCancels) runCancel(cancelSend);
|
|
1912
|
+
if (driverAction === "cancel") session?.cancel();
|
|
1913
|
+
return canceled;
|
|
1914
|
+
}
|
|
1915
|
+
function normalizeWebSocketSessionSendPolicy(policy) {
|
|
1916
|
+
if (policy?.kind !== "buffer") return { kind: "reject" };
|
|
1917
|
+
if (!Number.isFinite(policy.maxPending) || policy.maxPending < 1) {
|
|
1918
|
+
throw new Error("webSocketSession: buffer sendPolicy requires finite maxPending >= 1");
|
|
1919
|
+
}
|
|
1920
|
+
return { kind: "buffer", maxPending: Math.floor(policy.maxPending) };
|
|
1921
|
+
}
|
|
1922
|
+
function webSocketSessionEventNode(graph, command, request, name, policy, sendPolicy) {
|
|
1923
|
+
return graph.node(
|
|
1924
|
+
[command],
|
|
1925
|
+
(ctx) => {
|
|
1926
|
+
const state = initWebSocketSessionState(ctx);
|
|
1927
|
+
const emitOutbound = (fact) => {
|
|
1928
|
+
ctx.down([["DATA", { kind: "outbound", fact }]]);
|
|
1929
|
+
};
|
|
1930
|
+
const emitOutboundCanceled = (items, reason) => {
|
|
1931
|
+
for (const item of items) {
|
|
1932
|
+
emitOutbound({
|
|
1933
|
+
kind: "canceled",
|
|
1934
|
+
seq: item.seq,
|
|
1935
|
+
message: item.message,
|
|
1936
|
+
reason
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
const emitOutboundRejected = (items, error) => {
|
|
1941
|
+
const payload = errorPayload(error);
|
|
1942
|
+
for (const item of items) {
|
|
1943
|
+
emitOutbound({
|
|
1944
|
+
kind: "rejected",
|
|
1945
|
+
seq: item.seq,
|
|
1946
|
+
message: item.message,
|
|
1947
|
+
error: payload
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
const retryOrExhaust = (attempt, error) => {
|
|
1952
|
+
const payload = errorPayload(error);
|
|
1953
|
+
const canceled = cleanupWebSocketConnection(state);
|
|
1954
|
+
emitOutboundCanceled(canceled, `${name}: connection cleanup`);
|
|
1955
|
+
if (shouldRetry(policy, attempt)) {
|
|
1956
|
+
const nextAttempt = attempt + 1;
|
|
1957
|
+
const delayMs = nextRetryDelayMs(policy, nextAttempt) ?? 0;
|
|
1958
|
+
ctx.down([
|
|
1959
|
+
[
|
|
1960
|
+
"DATA",
|
|
1961
|
+
{
|
|
1962
|
+
kind: "retry",
|
|
1963
|
+
attempt,
|
|
1964
|
+
nextAttempt,
|
|
1965
|
+
delayMs,
|
|
1966
|
+
error: payload
|
|
1967
|
+
}
|
|
1968
|
+
]
|
|
1969
|
+
]);
|
|
1970
|
+
const timerId = state.seq++;
|
|
1971
|
+
const timer = setTimeout(() => {
|
|
1972
|
+
state.timers.delete(timerId);
|
|
1973
|
+
if (!state.active) return;
|
|
1974
|
+
start(nextAttempt);
|
|
1975
|
+
}, delayMs);
|
|
1976
|
+
state.timers.set(timerId, timer);
|
|
1977
|
+
} else {
|
|
1978
|
+
emitOutboundRejected(state.pendingOutbound, payload);
|
|
1979
|
+
state.pendingOutbound = [];
|
|
1980
|
+
ctx.down([
|
|
1981
|
+
[
|
|
1982
|
+
"DATA",
|
|
1983
|
+
{ kind: "exhausted", attempt, error: payload }
|
|
1984
|
+
]
|
|
1985
|
+
]);
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
const start = (attempt) => {
|
|
1989
|
+
if (!state.active || state.connected || state.currentAttempt !== 0 || state.timers.size > 0) {
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const driver = ctx.environment().webSocketDriver();
|
|
1993
|
+
state.currentAttempt = attempt;
|
|
1994
|
+
ctx.down([
|
|
1995
|
+
[
|
|
1996
|
+
"DATA",
|
|
1997
|
+
{
|
|
1998
|
+
kind: "attempt",
|
|
1999
|
+
attempt,
|
|
2000
|
+
maxAttempts: policy.maxAttempts
|
|
2001
|
+
}
|
|
2002
|
+
]
|
|
2003
|
+
]);
|
|
2004
|
+
const connectSession = driver?.connectSession;
|
|
2005
|
+
if (connectSession === void 0) {
|
|
2006
|
+
retryOrExhaust(attempt, `${name}: missing WebSocket session capability`);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
let session;
|
|
2010
|
+
try {
|
|
2011
|
+
session = connectSession.call(driver, request, (event) => {
|
|
2012
|
+
if (!state.active || state.currentAttempt !== attempt) return;
|
|
2013
|
+
if (event.kind === "event") {
|
|
2014
|
+
const ws = event.event;
|
|
2015
|
+
if (ws.kind === "open") {
|
|
2016
|
+
state.connected = true;
|
|
2017
|
+
ctx.down([["DATA", { kind: "open", attempt }]]);
|
|
2018
|
+
flushPending();
|
|
2019
|
+
} else if (ws.kind === "text" || ws.kind === "binary") {
|
|
2020
|
+
ctx.down([
|
|
2021
|
+
[
|
|
2022
|
+
"DATA",
|
|
2023
|
+
{
|
|
2024
|
+
kind: "message",
|
|
2025
|
+
message: ws
|
|
2026
|
+
}
|
|
2027
|
+
]
|
|
2028
|
+
]);
|
|
2029
|
+
} else if (ws.kind === "close") {
|
|
2030
|
+
const normalClose = ws.code === 1e3;
|
|
2031
|
+
const canceled = cleanupWebSocketConnection(state);
|
|
2032
|
+
emitOutboundCanceled(canceled, `${name}: session closed`);
|
|
2033
|
+
ctx.down([
|
|
2034
|
+
[
|
|
2035
|
+
"DATA",
|
|
2036
|
+
{
|
|
2037
|
+
kind: "closed",
|
|
2038
|
+
code: ws.code,
|
|
2039
|
+
reason: ws.reason
|
|
2040
|
+
}
|
|
2041
|
+
]
|
|
2042
|
+
]);
|
|
2043
|
+
if (!normalClose) {
|
|
2044
|
+
retryOrExhaust(attempt, {
|
|
2045
|
+
kind: "close",
|
|
2046
|
+
code: ws.code,
|
|
2047
|
+
reason: ws.reason
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
} else if (event.kind === "error") {
|
|
2052
|
+
retryOrExhaust(attempt, event.error);
|
|
2053
|
+
} else if (event.kind === "complete") {
|
|
2054
|
+
retryOrExhaust(attempt, `${name}: connection completed`);
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
retryOrExhaust(attempt, error);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
if (session === void 0) {
|
|
2062
|
+
retryOrExhaust(attempt, `${name}: missing WebSocket session capability`);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (state.active && state.currentAttempt === attempt) {
|
|
2066
|
+
state.currentSession = session;
|
|
2067
|
+
} else {
|
|
2068
|
+
session.cancel();
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
function sendNow(item) {
|
|
2072
|
+
if (!state.connected) {
|
|
2073
|
+
emitOutbound({
|
|
2074
|
+
kind: "rejected",
|
|
2075
|
+
seq: item.seq,
|
|
2076
|
+
message: item.message,
|
|
2077
|
+
error: `${name}: session is not open`
|
|
2078
|
+
});
|
|
2079
|
+
ctx.down([
|
|
2080
|
+
[
|
|
2081
|
+
"DATA",
|
|
2082
|
+
{
|
|
2083
|
+
kind: "error",
|
|
2084
|
+
error: `${name}: session is not open`
|
|
2085
|
+
}
|
|
2086
|
+
]
|
|
2087
|
+
]);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const session = state.currentSession;
|
|
2091
|
+
if (session === void 0) {
|
|
2092
|
+
emitOutbound({
|
|
2093
|
+
kind: "rejected",
|
|
2094
|
+
seq: item.seq,
|
|
2095
|
+
message: item.message,
|
|
2096
|
+
error: `${name}: missing WebSocket session capability`
|
|
2097
|
+
});
|
|
2098
|
+
ctx.down([
|
|
2099
|
+
[
|
|
2100
|
+
"DATA",
|
|
2101
|
+
{
|
|
2102
|
+
kind: "error",
|
|
2103
|
+
error: `${name}: missing WebSocket session capability`
|
|
2104
|
+
}
|
|
2105
|
+
]
|
|
2106
|
+
]);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const sendId = state.seq++;
|
|
2110
|
+
state.liveSends.set(sendId, item);
|
|
2111
|
+
emitOutbound({ kind: "sending", seq: item.seq, message: item.message });
|
|
2112
|
+
let done = false;
|
|
2113
|
+
let cancel;
|
|
2114
|
+
try {
|
|
2115
|
+
cancel = session.send(item.message, (result) => {
|
|
2116
|
+
if (done || !state.active || !state.liveSends.has(sendId)) return;
|
|
2117
|
+
done = true;
|
|
2118
|
+
state.liveSends.delete(sendId);
|
|
2119
|
+
state.sendCancels.delete(sendId);
|
|
2120
|
+
if (result.ok) {
|
|
2121
|
+
emitOutbound({ kind: "sent", seq: item.seq, message: item.message });
|
|
2122
|
+
ctx.down([
|
|
2123
|
+
["DATA", { kind: "sent", message: item.message }]
|
|
2124
|
+
]);
|
|
2125
|
+
} else {
|
|
2126
|
+
const payload = errorPayload(result.error);
|
|
2127
|
+
emitOutbound({
|
|
2128
|
+
kind: "rejected",
|
|
2129
|
+
seq: item.seq,
|
|
2130
|
+
message: item.message,
|
|
2131
|
+
error: payload
|
|
2132
|
+
});
|
|
2133
|
+
ctx.down([
|
|
2134
|
+
[
|
|
2135
|
+
"DATA",
|
|
2136
|
+
{
|
|
2137
|
+
kind: "error",
|
|
2138
|
+
error: payload
|
|
2139
|
+
}
|
|
2140
|
+
]
|
|
2141
|
+
]);
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
state.liveSends.delete(sendId);
|
|
2146
|
+
const payload = errorPayload(error);
|
|
2147
|
+
emitOutbound({
|
|
2148
|
+
kind: "rejected",
|
|
2149
|
+
seq: item.seq,
|
|
2150
|
+
message: item.message,
|
|
2151
|
+
error: payload
|
|
2152
|
+
});
|
|
2153
|
+
ctx.down([["DATA", { kind: "error", error: payload }]]);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (cancel !== void 0 && !done && state.active && state.liveSends.has(sendId)) {
|
|
2157
|
+
state.sendCancels.set(sendId, cancel);
|
|
2158
|
+
} else if (cancel !== void 0 && !state.active) runCancel(cancel);
|
|
2159
|
+
}
|
|
2160
|
+
function flushPending() {
|
|
2161
|
+
const pending = state.pendingOutbound;
|
|
2162
|
+
state.pendingOutbound = [];
|
|
2163
|
+
for (let index = 0; index < pending.length; index++) {
|
|
2164
|
+
const item = pending[index];
|
|
2165
|
+
if (!state.connected || state.currentSession === void 0) {
|
|
2166
|
+
emitOutboundCanceled(pending.slice(index), `${name}: session closed`);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
sendNow(item);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
const send = (message) => {
|
|
2173
|
+
const item = { seq: state.outboundSeq++, message };
|
|
2174
|
+
if (state.connected) {
|
|
2175
|
+
sendNow(item);
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
if (sendPolicy.kind === "buffer") {
|
|
2179
|
+
if (state.pendingOutbound.length >= sendPolicy.maxPending) {
|
|
2180
|
+
emitOutbound({
|
|
2181
|
+
kind: "rejected",
|
|
2182
|
+
seq: item.seq,
|
|
2183
|
+
message,
|
|
2184
|
+
error: `${name}: outbound buffer full`
|
|
2185
|
+
});
|
|
2186
|
+
ctx.down([
|
|
2187
|
+
[
|
|
2188
|
+
"DATA",
|
|
2189
|
+
{
|
|
2190
|
+
kind: "error",
|
|
2191
|
+
error: `${name}: outbound buffer full`
|
|
2192
|
+
}
|
|
2193
|
+
]
|
|
2194
|
+
]);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
state.pendingOutbound.push(item);
|
|
2198
|
+
emitOutbound({ kind: "queued", seq: item.seq, message });
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
emitOutbound({
|
|
2202
|
+
kind: "rejected",
|
|
2203
|
+
seq: item.seq,
|
|
2204
|
+
message,
|
|
2205
|
+
error: `${name}: session is not open`
|
|
2206
|
+
});
|
|
2207
|
+
ctx.down([
|
|
2208
|
+
[
|
|
2209
|
+
"DATA",
|
|
2210
|
+
{
|
|
2211
|
+
kind: "error",
|
|
2212
|
+
error: `${name}: session is not open`
|
|
2213
|
+
}
|
|
2214
|
+
]
|
|
2215
|
+
]);
|
|
2216
|
+
};
|
|
2217
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2218
|
+
const commandValue = raw;
|
|
2219
|
+
if (commandValue.kind === "start") start(1);
|
|
2220
|
+
else if (commandValue.kind === "send") send(commandValue.message);
|
|
2221
|
+
else {
|
|
2222
|
+
cancelRetryTimers(state);
|
|
2223
|
+
ctx.down([
|
|
2224
|
+
[
|
|
2225
|
+
"DATA",
|
|
2226
|
+
{
|
|
2227
|
+
kind: "closing",
|
|
2228
|
+
code: commandValue.code,
|
|
2229
|
+
reason: commandValue.reason
|
|
2230
|
+
}
|
|
2231
|
+
]
|
|
2232
|
+
]);
|
|
2233
|
+
const { canceled, session } = closeWebSocketSessionState(state, "none");
|
|
2234
|
+
emitOutboundCanceled(canceled, `${name}: session closed`);
|
|
2235
|
+
session?.close(commandValue.code, commandValue.reason);
|
|
2236
|
+
ctx.down([
|
|
2237
|
+
[
|
|
2238
|
+
"DATA",
|
|
2239
|
+
{
|
|
2240
|
+
kind: "closed",
|
|
2241
|
+
code: commandValue.code,
|
|
2242
|
+
reason: commandValue.reason
|
|
2243
|
+
}
|
|
2244
|
+
]
|
|
2245
|
+
]);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
},
|
|
2249
|
+
{ name: `${name}/events`, factory: "webSocketSessionEvents" }
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
function webSocketSessionNodes(graph, command, events, name, policy) {
|
|
2253
|
+
const inbound = graph.node(
|
|
2254
|
+
[events],
|
|
2255
|
+
(ctx) => {
|
|
2256
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2257
|
+
const event = raw;
|
|
2258
|
+
if (event.kind === "message") ctx.down([["DATA", event.message]]);
|
|
2259
|
+
}
|
|
2260
|
+
},
|
|
2261
|
+
{ name: `${name}/inbound`, factory: "webSocketSessionInbound" }
|
|
2262
|
+
);
|
|
2263
|
+
const lifecycle = graph.node(
|
|
2264
|
+
[events],
|
|
2265
|
+
(ctx) => {
|
|
2266
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2267
|
+
const event = raw;
|
|
2268
|
+
if (event.kind === "attempt") {
|
|
2269
|
+
ctx.down([
|
|
2270
|
+
[
|
|
2271
|
+
"DATA",
|
|
2272
|
+
{
|
|
2273
|
+
kind: "starting",
|
|
2274
|
+
attempt: event.attempt,
|
|
2275
|
+
maxAttempts: event.maxAttempts
|
|
2276
|
+
}
|
|
2277
|
+
]
|
|
2278
|
+
]);
|
|
2279
|
+
} else if (event.kind === "open") {
|
|
2280
|
+
ctx.down([["DATA", { kind: "open", attempt: event.attempt }]]);
|
|
2281
|
+
} else if (event.kind === "sent") {
|
|
2282
|
+
ctx.down([["DATA", { kind: "sent", message: event.message }]]);
|
|
2283
|
+
} else if (event.kind === "closing" || event.kind === "closed") {
|
|
2284
|
+
ctx.down([["DATA", event]]);
|
|
2285
|
+
} else if (event.kind === "retry") {
|
|
2286
|
+
ctx.down([
|
|
2287
|
+
[
|
|
2288
|
+
"DATA",
|
|
2289
|
+
{
|
|
2290
|
+
kind: "retrying",
|
|
2291
|
+
attempt: event.attempt,
|
|
2292
|
+
nextAttempt: event.nextAttempt,
|
|
2293
|
+
delayMs: event.delayMs,
|
|
2294
|
+
error: event.error
|
|
2295
|
+
}
|
|
2296
|
+
]
|
|
2297
|
+
]);
|
|
2298
|
+
} else if (event.kind === "exhausted") {
|
|
2299
|
+
ctx.down([["DATA", event]]);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
},
|
|
2303
|
+
{ name: `${name}/lifecycle`, factory: "webSocketSessionLifecycle" }
|
|
2304
|
+
);
|
|
2305
|
+
const outbound = graph.node(
|
|
2306
|
+
[events],
|
|
2307
|
+
(ctx) => {
|
|
2308
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2309
|
+
const event = raw;
|
|
2310
|
+
if (event.kind === "outbound") ctx.down([["DATA", event.fact]]);
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
{ name: `${name}/outbound`, factory: "webSocketSessionOutbound" }
|
|
2314
|
+
);
|
|
2315
|
+
const status = graph.node(
|
|
2316
|
+
[events],
|
|
2317
|
+
(ctx) => {
|
|
2318
|
+
let next = ctx.state.get() ?? {
|
|
2319
|
+
state: "idle",
|
|
2320
|
+
attempt: 0,
|
|
2321
|
+
maxAttempts: policy.maxAttempts,
|
|
2322
|
+
sent: 0,
|
|
2323
|
+
received: 0,
|
|
2324
|
+
errors: 0
|
|
2325
|
+
};
|
|
2326
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2327
|
+
const event = raw;
|
|
2328
|
+
if (event.kind === "attempt") {
|
|
2329
|
+
next = {
|
|
2330
|
+
...next,
|
|
2331
|
+
state: "connecting",
|
|
2332
|
+
attempt: event.attempt,
|
|
2333
|
+
maxAttempts: event.maxAttempts,
|
|
2334
|
+
lastDelayMs: void 0
|
|
2335
|
+
};
|
|
2336
|
+
} else if (event.kind === "open") {
|
|
2337
|
+
next = { ...next, state: "open", attempt: event.attempt, lastDelayMs: void 0 };
|
|
2338
|
+
} else if (event.kind === "message") {
|
|
2339
|
+
next = { ...next, received: next.received + 1 };
|
|
2340
|
+
} else if (event.kind === "sent") {
|
|
2341
|
+
next = { ...next, sent: next.sent + 1 };
|
|
2342
|
+
} else if (event.kind === "closing") {
|
|
2343
|
+
next = { ...next, state: "closing" };
|
|
2344
|
+
} else if (event.kind === "closed") {
|
|
2345
|
+
next = { ...next, state: "closed" };
|
|
2346
|
+
} else if (event.kind === "retry") {
|
|
2347
|
+
next = {
|
|
2348
|
+
...next,
|
|
2349
|
+
state: "waiting",
|
|
2350
|
+
attempt: event.attempt,
|
|
2351
|
+
errors: next.errors + 1,
|
|
2352
|
+
lastDelayMs: event.delayMs
|
|
2353
|
+
};
|
|
2354
|
+
} else if (event.kind === "error") {
|
|
2355
|
+
next = {
|
|
2356
|
+
...next,
|
|
2357
|
+
state: "errored",
|
|
2358
|
+
attempt: event.attempt ?? next.attempt,
|
|
2359
|
+
errors: next.errors + 1
|
|
2360
|
+
};
|
|
2361
|
+
} else if (event.kind === "exhausted") {
|
|
2362
|
+
next = {
|
|
2363
|
+
...next,
|
|
2364
|
+
state: "exhausted",
|
|
2365
|
+
attempt: event.attempt,
|
|
2366
|
+
errors: next.errors + 1
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
ctx.state.set(next);
|
|
2371
|
+
ctx.down([["DATA", next]]);
|
|
2372
|
+
},
|
|
2373
|
+
{ name: `${name}/status`, factory: "webSocketSessionStatus" }
|
|
2374
|
+
);
|
|
2375
|
+
const errors = graph.node(
|
|
2376
|
+
[events],
|
|
2377
|
+
(ctx) => {
|
|
2378
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2379
|
+
const event = raw;
|
|
2380
|
+
if ("error" in event) ctx.down([["DATA", event.error]]);
|
|
2381
|
+
}
|
|
2382
|
+
},
|
|
2383
|
+
{ name: `${name}/errors`, factory: "webSocketSessionErrors" }
|
|
2384
|
+
);
|
|
2385
|
+
const attempts = graph.node(
|
|
2386
|
+
[events],
|
|
2387
|
+
(ctx) => {
|
|
2388
|
+
for (const raw of depBatch(ctx, 0) ?? []) {
|
|
2389
|
+
const event = raw;
|
|
2390
|
+
if (event.kind === "attempt") ctx.down([["DATA", event.attempt]]);
|
|
2391
|
+
}
|
|
2392
|
+
},
|
|
2393
|
+
{ name: `${name}/attempts`, factory: "webSocketSessionAttempts" }
|
|
2394
|
+
);
|
|
2395
|
+
return {
|
|
2396
|
+
command,
|
|
2397
|
+
inbound,
|
|
2398
|
+
lifecycle,
|
|
2399
|
+
outbound,
|
|
2400
|
+
status,
|
|
2401
|
+
errors,
|
|
2402
|
+
attempts,
|
|
2403
|
+
start: () => command.down([["DATA", { kind: "start" }]]),
|
|
2404
|
+
send: (message) => command.down([
|
|
2405
|
+
[
|
|
2406
|
+
"DATA",
|
|
2407
|
+
{
|
|
2408
|
+
kind: "send",
|
|
2409
|
+
message: typeof message === "string" || message instanceof Uint8Array ? { data: message } : message
|
|
2410
|
+
}
|
|
2411
|
+
]
|
|
2412
|
+
]),
|
|
2413
|
+
close: (code, reason) => command.down([["DATA", { kind: "close", code, reason }]])
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function webSocketSession(graph, request, opts = {}) {
|
|
2417
|
+
const name = opts.name ?? "webSocketSession";
|
|
2418
|
+
const policy = opts.retry ?? retryPolicy();
|
|
2419
|
+
const sendPolicy = normalizeWebSocketSessionSendPolicy(opts.sendPolicy);
|
|
2420
|
+
const command = graph.node([], null, {
|
|
2421
|
+
name: `${name}/command`,
|
|
2422
|
+
factory: "webSocketSessionCommand"
|
|
2423
|
+
});
|
|
2424
|
+
const events = webSocketSessionEventNode(graph, command, request, name, policy, sendPolicy);
|
|
2425
|
+
return webSocketSessionNodes(graph, command, events, name, policy);
|
|
2426
|
+
}
|
|
2427
|
+
function outboundNode(graph, source, name, send, opts) {
|
|
2428
|
+
const policy = opts.retry ?? retryPolicy();
|
|
2429
|
+
return graph.node(
|
|
2430
|
+
[assertSourceLocal(source)],
|
|
2431
|
+
(ctx) => {
|
|
2432
|
+
const state = initSendState(ctx);
|
|
2433
|
+
const start = (value, attempt) => {
|
|
2434
|
+
if (!state.active) return;
|
|
2435
|
+
const id = state.seq++;
|
|
2436
|
+
ctx.down([
|
|
2437
|
+
["DATA", { kind: "attempt", value, attempt }]
|
|
2438
|
+
]);
|
|
2439
|
+
const failTerminal = (error) => {
|
|
2440
|
+
const payload = errorPayload(error);
|
|
2441
|
+
ctx.down([
|
|
2442
|
+
[
|
|
2443
|
+
"DATA",
|
|
2444
|
+
{ kind: "exhausted", value, attempt, error: payload }
|
|
2445
|
+
]
|
|
2446
|
+
]);
|
|
2447
|
+
ctx.down([["ERROR", payload]]);
|
|
2448
|
+
};
|
|
2449
|
+
let cancel;
|
|
2450
|
+
let done = false;
|
|
2451
|
+
try {
|
|
2452
|
+
cancel = send(ctx, value, (result) => {
|
|
2453
|
+
if (done || !state.active) return;
|
|
2454
|
+
done = true;
|
|
2455
|
+
state.cancels.delete(id);
|
|
2456
|
+
if (result.ok) {
|
|
2457
|
+
ctx.down([
|
|
2458
|
+
[
|
|
2459
|
+
"DATA",
|
|
2460
|
+
{ kind: "sent", value, attempt, result: result.value }
|
|
2461
|
+
]
|
|
2462
|
+
]);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
if (shouldRetry(policy, attempt)) {
|
|
2466
|
+
const nextAttempt = attempt + 1;
|
|
2467
|
+
const delayMs = nextRetryDelayMs(policy, nextAttempt) ?? 0;
|
|
2468
|
+
ctx.down([
|
|
2469
|
+
[
|
|
2470
|
+
"DATA",
|
|
2471
|
+
{
|
|
2472
|
+
kind: "retry",
|
|
2473
|
+
value,
|
|
2474
|
+
attempt,
|
|
2475
|
+
delayMs,
|
|
2476
|
+
error: errorPayload(result.error)
|
|
2477
|
+
}
|
|
2478
|
+
]
|
|
2479
|
+
]);
|
|
2480
|
+
const timer = setTimeout(() => {
|
|
2481
|
+
state.timers.delete(id);
|
|
2482
|
+
if (!state.active) return;
|
|
2483
|
+
start(value, nextAttempt);
|
|
2484
|
+
}, delayMs);
|
|
2485
|
+
state.timers.set(id, timer);
|
|
2486
|
+
} else {
|
|
2487
|
+
ctx.down([
|
|
2488
|
+
[
|
|
2489
|
+
"DATA",
|
|
2490
|
+
{
|
|
2491
|
+
kind: "exhausted",
|
|
2492
|
+
value,
|
|
2493
|
+
attempt,
|
|
2494
|
+
error: errorPayload(result.error)
|
|
2495
|
+
}
|
|
2496
|
+
]
|
|
2497
|
+
]);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
if (!done) {
|
|
2502
|
+
done = true;
|
|
2503
|
+
state.cancels.delete(id);
|
|
2504
|
+
failTerminal(error);
|
|
2505
|
+
}
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (cancel === void 0) {
|
|
2509
|
+
if (!done) {
|
|
2510
|
+
done = true;
|
|
2511
|
+
failTerminal(`${name}: missing environment driver capability`);
|
|
2512
|
+
}
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (!done && state.active) {
|
|
2516
|
+
state.cancels.set(id, cancel);
|
|
2517
|
+
} else if (!state.active) {
|
|
2518
|
+
runCancel(cancel);
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
for (const value of depBatch(ctx, 0) ?? []) start(value, 1);
|
|
2522
|
+
const terminal = ctx.terminal[0];
|
|
2523
|
+
if (terminal === true) {
|
|
2524
|
+
ctx.down([
|
|
2525
|
+
["DATA", { kind: "upstream-complete" }]
|
|
2526
|
+
]);
|
|
2527
|
+
} else if (terminal !== false && terminal !== void 0) {
|
|
2528
|
+
ctx.down([
|
|
2529
|
+
[
|
|
2530
|
+
"DATA",
|
|
2531
|
+
{ kind: "upstream-error", error: terminal }
|
|
2532
|
+
]
|
|
2533
|
+
]);
|
|
2534
|
+
}
|
|
2535
|
+
},
|
|
2536
|
+
{ name, factory: name }
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
function toHttp(graph, source, requestOf, opts = {}) {
|
|
2540
|
+
const name = opts.name ?? "toHttp";
|
|
2541
|
+
const events = outboundNode(
|
|
2542
|
+
graph,
|
|
2543
|
+
source,
|
|
2544
|
+
name,
|
|
2545
|
+
(ctx, value, callback) => {
|
|
2546
|
+
const request = requestOf(value);
|
|
2547
|
+
return ctx.environment().httpDriver()?.request(request, callback);
|
|
2548
|
+
},
|
|
2549
|
+
opts
|
|
2550
|
+
);
|
|
2551
|
+
return outboundNodes(graph, events, name);
|
|
2552
|
+
}
|
|
2553
|
+
function toProcess(graph, source, commandOf, opts = {}) {
|
|
2554
|
+
const name = opts.name ?? "toProcess";
|
|
2555
|
+
const events = outboundNode(
|
|
2556
|
+
graph,
|
|
2557
|
+
source,
|
|
2558
|
+
name,
|
|
2559
|
+
(ctx, value, callback) => {
|
|
2560
|
+
const command = commandOf(value);
|
|
2561
|
+
return ctx.environment().processDriver()?.run(command, callback);
|
|
2562
|
+
},
|
|
2563
|
+
opts
|
|
2564
|
+
);
|
|
2565
|
+
return outboundNodes(graph, events, name);
|
|
2566
|
+
}
|
|
2567
|
+
function toWebSocket(graph, source, request, sendOf, opts = {}) {
|
|
2568
|
+
const name = opts.name ?? "toWebSocket";
|
|
2569
|
+
const events = outboundNode(
|
|
2570
|
+
graph,
|
|
2571
|
+
source,
|
|
2572
|
+
name,
|
|
2573
|
+
(ctx, value, callback) => {
|
|
2574
|
+
const driver = ctx.environment().webSocketDriver();
|
|
2575
|
+
return driver?.send?.(request, sendOf(value), callback);
|
|
2576
|
+
},
|
|
2577
|
+
opts
|
|
2578
|
+
);
|
|
2579
|
+
return outboundNodes(graph, events, name);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// src/adapters/nestjs.ts
|
|
2583
|
+
var GRAPHREFLY_ROOT_GRAPH = /* @__PURE__ */ Symbol.for("graphrefly:root-graph");
|
|
2584
|
+
var GRAPHREFLY_MODULE_OPTIONS = /* @__PURE__ */ Symbol.for("graphrefly:module-options");
|
|
2585
|
+
var GRAPHREFLY_REQUEST_GRAPH = /* @__PURE__ */ Symbol.for("graphrefly:request-graph");
|
|
2586
|
+
var EVENT_HANDLERS = /* @__PURE__ */ new Map();
|
|
2587
|
+
var INTERVAL_HANDLERS = /* @__PURE__ */ new Map();
|
|
2588
|
+
var CRON_HANDLERS = /* @__PURE__ */ new Map();
|
|
2589
|
+
function getGraphToken(name) {
|
|
2590
|
+
return /* @__PURE__ */ Symbol.for(`graphrefly:graph:${name}`);
|
|
2591
|
+
}
|
|
2592
|
+
function getNodeToken(path) {
|
|
2593
|
+
return /* @__PURE__ */ Symbol.for(`graphrefly:node:${path}`);
|
|
2594
|
+
}
|
|
2595
|
+
function sameMeta(a, b) {
|
|
2596
|
+
const left = a;
|
|
2597
|
+
const right = b;
|
|
2598
|
+
const keys = Reflect.ownKeys(left);
|
|
2599
|
+
if (keys.length !== Reflect.ownKeys(right).length) return false;
|
|
2600
|
+
for (const key of keys) {
|
|
2601
|
+
if (!Object.is(left[key], right[key])) return false;
|
|
2602
|
+
}
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
function pushUniqueMeta(registry, ctor, item) {
|
|
2606
|
+
const existing = registry.get(ctor) ?? [];
|
|
2607
|
+
if (existing.some((current) => sameMeta(current, item))) return;
|
|
2608
|
+
registry.set(ctor, [...existing, item]);
|
|
2609
|
+
}
|
|
2610
|
+
function registerMeta(registry, meta) {
|
|
2611
|
+
return ((targetOrValue, contextOrKey) => {
|
|
2612
|
+
if (typeof contextOrKey === "object" && contextOrKey !== null) {
|
|
2613
|
+
const methodKey = contextOrKey.name;
|
|
2614
|
+
contextOrKey.addInitializer(function() {
|
|
2615
|
+
const ctor2 = this.constructor;
|
|
2616
|
+
pushUniqueMeta(registry, ctor2, meta(methodKey));
|
|
2617
|
+
});
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
const ctor = targetOrValue.constructor;
|
|
2621
|
+
pushUniqueMeta(registry, ctor, meta(contextOrKey));
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
function OnGraphEvent(nodeName) {
|
|
2625
|
+
return registerMeta(EVENT_HANDLERS, (methodKey) => ({ nodeName, methodKey }));
|
|
2626
|
+
}
|
|
2627
|
+
function GraphInterval(ms) {
|
|
2628
|
+
return registerMeta(INTERVAL_HANDLERS, (methodKey) => ({ ms, methodKey }));
|
|
2629
|
+
}
|
|
2630
|
+
function GraphCron(expr) {
|
|
2631
|
+
return registerMeta(CRON_HANDLERS, (methodKey) => ({ expr, methodKey }));
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// src/adapters/store.ts
|
|
2635
|
+
function nodeSnapshot(node) {
|
|
2636
|
+
return node.cache;
|
|
2637
|
+
}
|
|
2638
|
+
function subscribeNodeValues(node, run, opts = {}) {
|
|
2639
|
+
const read = opts.getSnapshot ?? (() => nodeSnapshot(node));
|
|
2640
|
+
if (opts.immediate) run(read());
|
|
2641
|
+
let subscribing = true;
|
|
2642
|
+
const skipSynchronousHandshakeData = opts.changesOnly || opts.immediate;
|
|
2643
|
+
const sink = (msg, delivery) => {
|
|
2644
|
+
switch (msg[0]) {
|
|
2645
|
+
case "DATA":
|
|
2646
|
+
if (skipSynchronousHandshakeData && subscribing && delivery === void 0) {
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
run(msg[1]);
|
|
2650
|
+
return;
|
|
2651
|
+
case "ERROR":
|
|
2652
|
+
opts.onError?.(msg[1]);
|
|
2653
|
+
return;
|
|
2654
|
+
case "COMPLETE":
|
|
2655
|
+
opts.onComplete?.();
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
const unsubscribe = node.subscribe(sink);
|
|
2660
|
+
subscribing = false;
|
|
2661
|
+
return unsubscribe;
|
|
2662
|
+
}
|
|
2663
|
+
function readableStore(node, opts = {}) {
|
|
2664
|
+
return {
|
|
2665
|
+
get: () => (opts.getSnapshot ?? (() => nodeSnapshot(node)))(),
|
|
2666
|
+
subscribe: (run) => subscribeNodeValues(node, run, { immediate: true, ...opts })
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function hasWritableSet(node) {
|
|
2670
|
+
return typeof node.set === "function";
|
|
2671
|
+
}
|
|
2672
|
+
function defaultWrite(node, value) {
|
|
2673
|
+
node.set(value);
|
|
2674
|
+
}
|
|
2675
|
+
function writableStore(node, opts = {}) {
|
|
2676
|
+
const read = opts.getSnapshot ?? (() => nodeSnapshot(node));
|
|
2677
|
+
const write = opts.write ?? (hasWritableSet(node) ? defaultWrite : void 0);
|
|
2678
|
+
if (!write) {
|
|
2679
|
+
throw new TypeError("writableStore: node must expose set(value) or opts.write");
|
|
2680
|
+
}
|
|
2681
|
+
const writable = node;
|
|
2682
|
+
return {
|
|
2683
|
+
get: read,
|
|
2684
|
+
subscribe: (run) => subscribeNodeValues(node, run, { immediate: true, ...opts }),
|
|
2685
|
+
set: (value) => write(writable, value),
|
|
2686
|
+
update: (fn) => write(writable, fn(read()))
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
function externalStore(node, opts = {}) {
|
|
2690
|
+
const read = opts.getSnapshot ?? (() => nodeSnapshot(node));
|
|
2691
|
+
return {
|
|
2692
|
+
getSnapshot: read,
|
|
2693
|
+
getServerSnapshot: read,
|
|
2694
|
+
subscribe: (onStoreChange) => subscribeNodeValues(node, () => onStoreChange(), {
|
|
2695
|
+
changesOnly: true,
|
|
2696
|
+
onError: opts.onError,
|
|
2697
|
+
onComplete: opts.onComplete
|
|
2698
|
+
})
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
var reactExternalStore = externalStore;
|
|
2702
|
+
var svelteReadableStore = readableStore;
|
|
2703
|
+
var svelteWritableStore = writableStore;
|
|
2704
|
+
function recordReadableStore(keysNode, factory, opts = {}) {
|
|
2705
|
+
const read = () => {
|
|
2706
|
+
const keys = keysNode.cache ?? [];
|
|
2707
|
+
const out = {};
|
|
2708
|
+
for (const key of keys) {
|
|
2709
|
+
const nodes = factory(key);
|
|
2710
|
+
const values = {};
|
|
2711
|
+
for (const field of Object.keys(nodes)) {
|
|
2712
|
+
values[field] = nodes[field].cache;
|
|
2713
|
+
}
|
|
2714
|
+
out[key] = values;
|
|
2715
|
+
}
|
|
2716
|
+
return out;
|
|
2717
|
+
};
|
|
2718
|
+
return {
|
|
2719
|
+
get: read,
|
|
2720
|
+
subscribe(run) {
|
|
2721
|
+
const entryUnsubs = [];
|
|
2722
|
+
const cleanupEntries = () => {
|
|
2723
|
+
for (const unsub of entryUnsubs.splice(0)) unsub();
|
|
2724
|
+
};
|
|
2725
|
+
const sync = () => {
|
|
2726
|
+
cleanupEntries();
|
|
2727
|
+
for (const key of keysNode.cache ?? []) {
|
|
2728
|
+
const nodes = factory(key);
|
|
2729
|
+
for (const field of Object.keys(nodes)) {
|
|
2730
|
+
entryUnsubs.push(
|
|
2731
|
+
subscribeNodeValues(nodes[field], () => run(read()), {
|
|
2732
|
+
changesOnly: true,
|
|
2733
|
+
onError: opts.onError,
|
|
2734
|
+
onComplete: opts.onComplete
|
|
2735
|
+
})
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
run(read());
|
|
2740
|
+
};
|
|
2741
|
+
const keysUnsub = subscribeNodeValues(keysNode, sync, {
|
|
2742
|
+
changesOnly: true,
|
|
2743
|
+
onError: opts.onError,
|
|
2744
|
+
onComplete: opts.onComplete
|
|
2745
|
+
});
|
|
2746
|
+
sync();
|
|
2747
|
+
return () => {
|
|
2748
|
+
keysUnsub();
|
|
2749
|
+
cleanupEntries();
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function zustandStore(node, initialState = nodeSnapshot(node), opts = {}) {
|
|
2755
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2756
|
+
const read = opts.getSnapshot ?? (() => nodeSnapshot(node));
|
|
2757
|
+
const write = opts.write ?? (hasWritableSet(node) ? defaultWrite : void 0);
|
|
2758
|
+
if (!write) {
|
|
2759
|
+
throw new TypeError("zustandStore: node must expose set(value) or opts.write");
|
|
2760
|
+
}
|
|
2761
|
+
const writable = node;
|
|
2762
|
+
const api = {
|
|
2763
|
+
getState: () => read(),
|
|
2764
|
+
setState(partial, replace) {
|
|
2765
|
+
const prev = api.getState();
|
|
2766
|
+
const next = typeof partial === "function" ? partial(prev) : partial;
|
|
2767
|
+
write(writable, replace ? next : { ...prev, ...next });
|
|
2768
|
+
},
|
|
2769
|
+
getInitialState: () => initialState,
|
|
2770
|
+
subscribe(listener) {
|
|
2771
|
+
let prev = api.getState();
|
|
2772
|
+
const unsub = subscribeNodeValues(
|
|
2773
|
+
node,
|
|
2774
|
+
(value) => {
|
|
2775
|
+
const next = value;
|
|
2776
|
+
listener(next, prev);
|
|
2777
|
+
prev = next;
|
|
2778
|
+
},
|
|
2779
|
+
{ changesOnly: true, onError: opts.onError, onComplete: opts.onComplete }
|
|
2780
|
+
);
|
|
2781
|
+
listeners.add(unsub);
|
|
2782
|
+
return () => {
|
|
2783
|
+
if (!listeners.delete(unsub)) return;
|
|
2784
|
+
unsub();
|
|
2785
|
+
};
|
|
2786
|
+
},
|
|
2787
|
+
destroy() {
|
|
2788
|
+
for (const unsub of [...listeners]) {
|
|
2789
|
+
listeners.delete(unsub);
|
|
2790
|
+
unsub();
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
return api;
|
|
2795
|
+
}
|
|
2796
|
+
function jotaiAtom(node, opts = {}) {
|
|
2797
|
+
const base = {
|
|
2798
|
+
get: () => (opts.getSnapshot ?? (() => nodeSnapshot(node)))(),
|
|
2799
|
+
subscribe: (callback) => subscribeNodeValues(node, callback, { changesOnly: true, ...opts }),
|
|
2800
|
+
_node: node
|
|
2801
|
+
};
|
|
2802
|
+
if (hasWritableSet(node) || typeof opts.write === "function") {
|
|
2803
|
+
const writable = writableStore(
|
|
2804
|
+
node,
|
|
2805
|
+
opts
|
|
2806
|
+
);
|
|
2807
|
+
return { ...base, set: writable.set, update: writable.update };
|
|
2808
|
+
}
|
|
2809
|
+
return base;
|
|
2810
|
+
}
|
|
2811
|
+
function nanoAtom(node, opts = {}) {
|
|
2812
|
+
const base = {
|
|
2813
|
+
get: () => (opts.getSnapshot ?? (() => nodeSnapshot(node)))(),
|
|
2814
|
+
subscribe: (callback) => subscribeNodeValues(node, callback, { immediate: true, ...opts }),
|
|
2815
|
+
listen: (callback) => subscribeNodeValues(node, callback, { changesOnly: true, ...opts }),
|
|
2816
|
+
_node: node
|
|
2817
|
+
};
|
|
2818
|
+
if (hasWritableSet(node) || typeof opts.write === "function") {
|
|
2819
|
+
const writable = writableStore(
|
|
2820
|
+
node,
|
|
2821
|
+
opts
|
|
2822
|
+
);
|
|
2823
|
+
return { ...base, set: writable.set, update: writable.update };
|
|
2824
|
+
}
|
|
2825
|
+
return base;
|
|
2826
|
+
}
|
|
2827
|
+
function signalFromNode(node, opts = {}) {
|
|
2828
|
+
const base = {
|
|
2829
|
+
get: () => (opts.getSnapshot ?? (() => nodeSnapshot(node)))(),
|
|
2830
|
+
subscribe: (callback) => subscribeNodeValues(node, callback, { changesOnly: true, ...opts }),
|
|
2831
|
+
_node: node
|
|
2832
|
+
};
|
|
2833
|
+
if (hasWritableSet(node) || typeof opts.write === "function") {
|
|
2834
|
+
const writable = writableStore(
|
|
2835
|
+
node,
|
|
2836
|
+
opts
|
|
2837
|
+
);
|
|
2838
|
+
return { ...base, set: writable.set, update: writable.update };
|
|
2839
|
+
}
|
|
2840
|
+
return base;
|
|
2841
|
+
}
|
|
2842
|
+
export {
|
|
2843
|
+
AGENTIC_MEMORY_RECORD_CHANGE_FORMAT,
|
|
2844
|
+
AGENTIC_MEMORY_RECORD_SNAPSHOT_FORMAT,
|
|
2845
|
+
AGENTIC_MEMORY_RECORD_STORAGE_FRAME_VERSION,
|
|
2846
|
+
CRON_HANDLERS,
|
|
2847
|
+
EVENT_HANDLERS,
|
|
2848
|
+
GRAPHREFLY_MODULE_OPTIONS,
|
|
2849
|
+
GRAPHREFLY_REQUEST_GRAPH,
|
|
2850
|
+
GRAPHREFLY_ROOT_GRAPH,
|
|
2851
|
+
GraphCron,
|
|
2852
|
+
GraphInterval,
|
|
2853
|
+
INTERVAL_HANDLERS,
|
|
2854
|
+
OnGraphEvent,
|
|
2855
|
+
agenticMemoryRecordChangeFrame,
|
|
2856
|
+
agenticMemoryRecordSnapshotFrame,
|
|
2857
|
+
agenticMemoryRecordsSnapshotKey,
|
|
2858
|
+
assertAgenticMemoryRecordChangeFrame,
|
|
2859
|
+
assertAgenticMemoryRecordSnapshotFrame,
|
|
2860
|
+
externalStore,
|
|
2861
|
+
getGraphToken,
|
|
2862
|
+
getNodeToken,
|
|
2863
|
+
jotaiAtom,
|
|
2864
|
+
loadAgenticMemoryRecordsState,
|
|
2865
|
+
nanoAtom,
|
|
2866
|
+
nodeSnapshot,
|
|
2867
|
+
openPersistentAgenticMemoryRecords,
|
|
2868
|
+
openPersistentReactiveIndex,
|
|
2869
|
+
openPersistentReactiveList,
|
|
2870
|
+
openPersistentReactiveLog,
|
|
2871
|
+
openPersistentReactiveMap,
|
|
2872
|
+
persistAgenticMemoryRecords,
|
|
2873
|
+
persistReactiveCollection,
|
|
2874
|
+
reactExternalStore,
|
|
2875
|
+
readableStore,
|
|
2876
|
+
recordReadableStore,
|
|
2877
|
+
remoteCall,
|
|
2878
|
+
remoteResponder,
|
|
2879
|
+
remoteResponderHandler,
|
|
2880
|
+
signalFromNode,
|
|
2881
|
+
subscribeNodeValues,
|
|
2882
|
+
svelteReadableStore,
|
|
2883
|
+
svelteWritableStore,
|
|
2884
|
+
toHttp,
|
|
2885
|
+
toProcess,
|
|
2886
|
+
toWebSocket,
|
|
2887
|
+
webSocketSession,
|
|
2888
|
+
wireBridge,
|
|
2889
|
+
wireBridgeEnvelope,
|
|
2890
|
+
wireBridgeIdempotencyKey,
|
|
2891
|
+
writableStore,
|
|
2892
|
+
zustandStore
|
|
2893
|
+
};
|
|
2894
|
+
//# sourceMappingURL=index.js.map
|