@ontahi/react 1.0.0-alpha.12 → 1.0.0-alpha.14
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/README.md +90 -5
- package/dist/actions/fetch-operation-bridge-adapter.d.ts.map +1 -1
- package/dist/actions/fetch-operation-bridge-adapter.js +30 -13
- package/dist/actions/fetch-operation-bridge-adapter.js.map +1 -1
- package/dist/actions/operation-bridge.d.ts +2 -0
- package/dist/actions/operation-bridge.d.ts.map +1 -1
- package/dist/actions/operation-bridge.js.map +1 -1
- package/dist/graph/canonical-request-invalidation.d.ts +14 -0
- package/dist/graph/canonical-request-invalidation.d.ts.map +1 -0
- package/dist/graph/canonical-request-invalidation.js +42 -0
- package/dist/graph/canonical-request-invalidation.js.map +1 -0
- package/dist/graph/context.d.ts +9 -2
- package/dist/graph/context.d.ts.map +1 -1
- package/dist/graph/context.js +38 -3
- package/dist/graph/context.js.map +1 -1
- package/dist/graph/executor.d.ts +18 -1
- package/dist/graph/executor.d.ts.map +1 -1
- package/dist/graph/executor.js.map +1 -1
- package/dist/graph/fetch-graph-client.d.ts +2 -1
- package/dist/graph/fetch-graph-client.d.ts.map +1 -1
- package/dist/graph/fetch-graph-client.js.map +1 -1
- package/dist/graph/fetch-graph-runtime.d.ts.map +1 -1
- package/dist/graph/fetch-graph-runtime.js +53 -1
- package/dist/graph/fetch-graph-runtime.js.map +1 -1
- package/dist/graph/fetch-runtime-transport.d.ts.map +1 -1
- package/dist/graph/fetch-runtime-transport.js +1 -0
- package/dist/graph/fetch-runtime-transport.js.map +1 -1
- package/dist/graph/graph-hooks.d.ts +2 -1
- package/dist/graph/graph-hooks.d.ts.map +1 -1
- package/dist/graph/graph-hooks.js +161 -62
- package/dist/graph/graph-hooks.js.map +1 -1
- package/dist/graph/index.d.ts +3 -0
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/index.js +3 -0
- package/dist/graph/index.js.map +1 -1
- package/dist/graph/model-command-hooks.d.ts +2 -0
- package/dist/graph/model-command-hooks.d.ts.map +1 -0
- package/dist/graph/model-command-hooks.js +9 -0
- package/dist/graph/model-command-hooks.js.map +1 -0
- package/dist/graph/operation-hooks.d.ts.map +1 -1
- package/dist/graph/operation-hooks.js +91 -9
- package/dist/graph/operation-hooks.js.map +1 -1
- package/dist/graph/operation-types.d.ts +1 -0
- package/dist/graph/operation-types.d.ts.map +1 -1
- package/dist/graph/operation-types.js.map +1 -1
- package/dist/graph/runtime-graph-client.d.ts +3 -2
- package/dist/graph/runtime-graph-client.d.ts.map +1 -1
- package/dist/graph/runtime-graph-client.js +2 -1
- package/dist/graph/runtime-graph-client.js.map +1 -1
- package/dist/graph/semantic-invalidation.d.ts +27 -0
- package/dist/graph/semantic-invalidation.d.ts.map +1 -0
- package/dist/graph/semantic-invalidation.js +55 -0
- package/dist/graph/semantic-invalidation.js.map +1 -0
- package/dist/graph/websocket-runtime-transport.d.ts +5 -2
- package/dist/graph/websocket-runtime-transport.d.ts.map +1 -1
- package/dist/graph/websocket-runtime-transport.js +160 -46
- package/dist/graph/websocket-runtime-transport.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { mayAffectGraphRead, } from '@ontahi/core/data-graph';
|
|
3
|
+
export const ONTAHI_GRAPH_READ_META_KEY = 'ontahi.graphRead';
|
|
4
|
+
export const withCanonicalGraphReadMeta = (meta, request, lifecycle = 'once') => ({
|
|
5
|
+
...meta,
|
|
6
|
+
[ONTAHI_GRAPH_READ_META_KEY]: { kind: 'canonical', request, lifecycle },
|
|
7
|
+
});
|
|
8
|
+
export const withUnknownGraphReadMeta = (meta, lifecycle = 'once') => ({
|
|
9
|
+
...meta,
|
|
10
|
+
[ONTAHI_GRAPH_READ_META_KEY]: { kind: 'unknown', lifecycle },
|
|
11
|
+
});
|
|
12
|
+
export const getCanonicalGraphReadFromMeta = (meta) => {
|
|
13
|
+
const dependency = meta?.[ONTAHI_GRAPH_READ_META_KEY];
|
|
14
|
+
return dependency &&
|
|
15
|
+
typeof dependency === 'object' &&
|
|
16
|
+
'kind' in dependency &&
|
|
17
|
+
dependency.kind === 'canonical' &&
|
|
18
|
+
'request' in dependency
|
|
19
|
+
? dependency.request
|
|
20
|
+
: undefined;
|
|
21
|
+
};
|
|
22
|
+
export const invalidateSemanticGraphReads = async (queryClient, committed, options) => {
|
|
23
|
+
const affected = queryClient
|
|
24
|
+
.getQueryCache()
|
|
25
|
+
.findAll()
|
|
26
|
+
.flatMap(query => {
|
|
27
|
+
const dependency = query.meta?.[ONTAHI_GRAPH_READ_META_KEY];
|
|
28
|
+
if (dependency &&
|
|
29
|
+
typeof dependency === 'object' &&
|
|
30
|
+
'lifecycle' in dependency &&
|
|
31
|
+
dependency.lifecycle === 'live' &&
|
|
32
|
+
query.state.fetchStatus === 'fetching') {
|
|
33
|
+
// Its observation already receives committed revisions from the runtime. Restarting the
|
|
34
|
+
// query here would create a second subscription and a redundant initial snapshot.
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
if (dependency &&
|
|
38
|
+
typeof dependency === 'object' &&
|
|
39
|
+
'kind' in dependency &&
|
|
40
|
+
dependency.kind === 'unknown') {
|
|
41
|
+
return committed.mutations.length > 0 ? [{ queryKey: query.queryKey }] : [];
|
|
42
|
+
}
|
|
43
|
+
const read = getCanonicalGraphReadFromMeta(query.meta);
|
|
44
|
+
return read && committed.mutations.some(mutation => mayAffectGraphRead(mutation, read))
|
|
45
|
+
? [{ queryKey: query.queryKey, request: read }]
|
|
46
|
+
: [];
|
|
47
|
+
});
|
|
48
|
+
const queryKeys = affected.map(item => item.queryKey);
|
|
49
|
+
if (options?.causedBy && options.associateRefreshes) {
|
|
50
|
+
options.associateRefreshes(affected.flatMap(item => (item.request ? [item.request] : [])), options.causedBy);
|
|
51
|
+
}
|
|
52
|
+
await Promise.all(queryKeys.map(queryKey => queryClient.invalidateQueries({ queryKey, exact: true })));
|
|
53
|
+
return { queryKeys };
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=semantic-invalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-invalidation.js","sourceRoot":"","sources":["../../src/graph/semantic-invalidation.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n mayAffectGraphRead,\n type CommittedMutationSet,\n type GraphReadRequest,\n} from '@ontahi/core/data-graph';\nimport type { RuntimeProtocolGraphRefreshCause } from '@ontahi/core/runtime/protocol';\nimport type { QueryClient, QueryKey, QueryMeta } from '@tanstack/react-query';\n\nexport const ONTAHI_GRAPH_READ_META_KEY = 'ontahi.graphRead' as const;\n\ntype OntahiGraphReadMeta = QueryMeta & {\n readonly [ONTAHI_GRAPH_READ_META_KEY]:\n | {\n readonly kind: 'canonical';\n readonly request: GraphReadRequest;\n readonly lifecycle: 'once' | 'live';\n }\n | { readonly kind: 'unknown'; readonly lifecycle: 'once' | 'live' };\n};\n\nexport type SemanticGraphReadInvalidation = {\n readonly queryKeys: readonly QueryKey[];\n};\n\nexport type SemanticGraphReadInvalidationOptions = {\n readonly causedBy?: RuntimeProtocolGraphRefreshCause;\n readonly associateRefreshes?: (\n requests: readonly GraphReadRequest[],\n causedBy: RuntimeProtocolGraphRefreshCause,\n ) => void;\n};\n\nexport const withCanonicalGraphReadMeta = (\n meta: QueryMeta | undefined,\n request: GraphReadRequest,\n lifecycle: 'once' | 'live' = 'once',\n): OntahiGraphReadMeta => ({\n ...meta,\n [ONTAHI_GRAPH_READ_META_KEY]: { kind: 'canonical', request, lifecycle },\n});\n\nexport const withUnknownGraphReadMeta = (\n meta: QueryMeta | undefined,\n lifecycle: 'once' | 'live' = 'once',\n): OntahiGraphReadMeta => ({\n ...meta,\n [ONTAHI_GRAPH_READ_META_KEY]: { kind: 'unknown', lifecycle },\n});\n\nexport const getCanonicalGraphReadFromMeta = (\n meta: QueryMeta | undefined,\n): GraphReadRequest | undefined => {\n const dependency = meta?.[ONTAHI_GRAPH_READ_META_KEY];\n return dependency &&\n typeof dependency === 'object' &&\n 'kind' in dependency &&\n dependency.kind === 'canonical' &&\n 'request' in dependency\n ? (dependency.request as GraphReadRequest)\n : undefined;\n};\n\nexport const invalidateSemanticGraphReads = async (\n queryClient: QueryClient,\n committed: CommittedMutationSet,\n options?: SemanticGraphReadInvalidationOptions,\n): Promise<SemanticGraphReadInvalidation> => {\n const affected: Array<{ readonly queryKey: QueryKey; readonly request?: GraphReadRequest }> =\n queryClient\n .getQueryCache()\n .findAll()\n .flatMap(query => {\n const dependency = query.meta?.[ONTAHI_GRAPH_READ_META_KEY];\n if (\n dependency &&\n typeof dependency === 'object' &&\n 'lifecycle' in dependency &&\n dependency.lifecycle === 'live' &&\n query.state.fetchStatus === 'fetching'\n ) {\n // Its observation already receives committed revisions from the runtime. Restarting the\n // query here would create a second subscription and a redundant initial snapshot.\n return [];\n }\n if (\n dependency &&\n typeof dependency === 'object' &&\n 'kind' in dependency &&\n dependency.kind === 'unknown'\n ) {\n return committed.mutations.length > 0 ? [{ queryKey: query.queryKey }] : [];\n }\n const read = getCanonicalGraphReadFromMeta(query.meta);\n return read && committed.mutations.some(mutation => mayAffectGraphRead(mutation, read))\n ? [{ queryKey: query.queryKey, request: read }]\n : [];\n });\n const queryKeys = affected.map(item => item.queryKey);\n if (options?.causedBy && options.associateRefreshes) {\n options.associateRefreshes(\n affected.flatMap(item => (item.request ? [item.request] : [])),\n options.causedBy,\n );\n }\n\n await Promise.all(\n queryKeys.map(queryKey => queryClient.invalidateQueries({ queryKey, exact: true })),\n );\n\n return { queryKeys };\n};\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DurableOperationObservationCapability, type GraphObservationCapability, type RuntimeTransport } from '@ontahi/core/runtime/protocol';
|
|
1
|
+
import { type DurableOperationObservationCapability, type EventObservationCapability, type GraphObservationCapability, type RuntimeTransport } from '@ontahi/core/runtime/protocol';
|
|
2
2
|
type WebSocketEvent = {
|
|
3
3
|
readonly data?: unknown;
|
|
4
4
|
readonly code?: number;
|
|
@@ -17,13 +17,16 @@ export type WebSocketRuntimeTransportOptions = {
|
|
|
17
17
|
readonly createWebSocket?: (url: string, protocols?: string | string[]) => RuntimeWebSocket;
|
|
18
18
|
readonly observationId?: () => string;
|
|
19
19
|
readonly handshakeTimeoutMs?: number;
|
|
20
|
+
/** Delay before restoring a durable Operation observation after a lost session. */
|
|
21
|
+
readonly reconnectDelayMs?: number;
|
|
20
22
|
readonly reportError?: (error: Error) => void;
|
|
21
23
|
};
|
|
22
24
|
export type WebSocketRuntimeTransport = RuntimeTransport<never> & {
|
|
23
25
|
readonly durableOperation: DurableOperationObservationCapability;
|
|
24
26
|
readonly graph: GraphObservationCapability<never>;
|
|
27
|
+
readonly events: EventObservationCapability<never>;
|
|
25
28
|
close(): void;
|
|
26
29
|
};
|
|
27
|
-
export declare const createWebSocketRuntimeTransport: ({ url, protocols, createWebSocket, observationId, handshakeTimeoutMs, reportError, }?: WebSocketRuntimeTransportOptions) => WebSocketRuntimeTransport;
|
|
30
|
+
export declare const createWebSocketRuntimeTransport: ({ url, protocols, createWebSocket, observationId, handshakeTimeoutMs, reconnectDelayMs, reportError, }?: WebSocketRuntimeTransportOptions) => WebSocketRuntimeTransport;
|
|
28
31
|
export {};
|
|
29
32
|
//# sourceMappingURL=websocket-runtime-transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-runtime-transport.d.ts","sourceRoot":"","sources":["../../src/graph/websocket-runtime-transport.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,qCAAqC,EAC1C,KAAK,0BAA0B,EAK/B,KAAK,gBAAgB,EAEtB,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"websocket-runtime-transport.d.ts","sourceRoot":"","sources":["../../src/graph/websocket-runtime-transport.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,qCAAqC,EAC1C,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAK/B,KAAK,gBAAgB,EAEtB,MAAM,+BAA+B,CAAC;AAGvC,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAChF,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;CACpF,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAC5F,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,gBAAgB,CAAC,KAAK,CAAC,GAAG;IAChE,QAAQ,CAAC,gBAAgB,EAAE,qCAAqC,CAAC;IACjE,QAAQ,CAAC,KAAK,EAAE,0BAA0B,CAAC,KAAK,CAAC,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC,KAAK,CAAC,CAAC;IACnD,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAkJF,eAAO,MAAM,+BAA+B,GAAI,yGAQ7C,gCAAqC,KAAG,yBA2iB1C,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { parseRuntimeProtocolResponse, parseRuntimeProtocolSessionServerFrame, RUNTIME_PROTOCOL_SESSION_NAME, RUNTIME_PROTOCOL_SESSION_VERSION, } from '@ontahi/core/runtime/protocol';
|
|
3
|
-
const createAsyncQueue = () => {
|
|
3
|
+
const createAsyncQueue = (capacity = Number.POSITIVE_INFINITY) => {
|
|
4
4
|
const values = [];
|
|
5
5
|
const waiters = [];
|
|
6
6
|
let ended = false;
|
|
@@ -31,6 +31,10 @@ const createAsyncQueue = () => {
|
|
|
31
31
|
const waiter = waiters.shift();
|
|
32
32
|
if (waiter)
|
|
33
33
|
waiter.resolve({ done: false, value });
|
|
34
|
+
else if (values.length >= capacity) {
|
|
35
|
+
failure = new Error('Runtime Protocol observation queue overflowed.');
|
|
36
|
+
settleWaiters();
|
|
37
|
+
}
|
|
34
38
|
else
|
|
35
39
|
values.push(value);
|
|
36
40
|
},
|
|
@@ -51,6 +55,9 @@ const createAsyncQueue = () => {
|
|
|
51
55
|
const SOCKET_OPEN = 1;
|
|
52
56
|
const DEFAULT_RUNTIME_PATH = '/runtime';
|
|
53
57
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000;
|
|
58
|
+
const DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
59
|
+
const MAX_DURABLE_OBSERVATION_RECONNECT_ATTEMPTS = 5;
|
|
60
|
+
const MAX_DURABLE_OBSERVATION_RECONNECT_DELAY_MS = 5000;
|
|
54
61
|
const terminalTaskStatuses = new Set(['completed', 'failed', 'cancelled']);
|
|
55
62
|
let fallbackObservationSequence = 0;
|
|
56
63
|
const defaultObservationId = () => globalThis.crypto?.randomUUID?.() ??
|
|
@@ -72,6 +79,8 @@ const createDefaultWebSocket = (url, protocols) => {
|
|
|
72
79
|
return new globalThis.WebSocket(url, protocols);
|
|
73
80
|
};
|
|
74
81
|
const abortError = (message) => new Error(message);
|
|
82
|
+
class RuntimeWebSocketSessionLostError extends Error {
|
|
83
|
+
}
|
|
75
84
|
const awaitWithSignal = async (promise, signal, message) => {
|
|
76
85
|
if (!signal)
|
|
77
86
|
return promise;
|
|
@@ -92,10 +101,13 @@ const awaitWithSignal = async (promise, signal, message) => {
|
|
|
92
101
|
});
|
|
93
102
|
});
|
|
94
103
|
};
|
|
95
|
-
export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocket = createDefaultWebSocket, observationId = defaultObservationId, handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, reportError, } = {}) => {
|
|
104
|
+
export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocket = createDefaultWebSocket, observationId = defaultObservationId, handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, reconnectDelayMs = DEFAULT_RECONNECT_DELAY_MS, reportError, } = {}) => {
|
|
96
105
|
if (!Number.isFinite(handshakeTimeoutMs) || handshakeTimeoutMs < 0) {
|
|
97
106
|
throw new TypeError('Runtime Protocol WebSocket handshake timeout must be non-negative.');
|
|
98
107
|
}
|
|
108
|
+
if (!Number.isFinite(reconnectDelayMs) || reconnectDelayMs < 0) {
|
|
109
|
+
throw new TypeError('Runtime Protocol WebSocket reconnect delay must be non-negative.');
|
|
110
|
+
}
|
|
99
111
|
let socket;
|
|
100
112
|
let connection;
|
|
101
113
|
let capabilities;
|
|
@@ -104,6 +116,17 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
104
116
|
const pendingRequests = new Map();
|
|
105
117
|
const durableObservations = new Map();
|
|
106
118
|
const graphObservations = new Map();
|
|
119
|
+
const eventObservations = new Map();
|
|
120
|
+
const prepareObservation = (queue, signal) => {
|
|
121
|
+
const id = observationId();
|
|
122
|
+
const abort = () => queue.end();
|
|
123
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
release: () => signal?.removeEventListener('abort', abort),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
const observationIdIsActive = (id) => durableObservations.has(id) || graphObservations.has(id) || eventObservations.has(id);
|
|
107
130
|
const send = (frame) => {
|
|
108
131
|
if (disposed)
|
|
109
132
|
throw new Error('Runtime Protocol WebSocket transport is closed.');
|
|
@@ -124,6 +147,9 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
124
147
|
for (const observation of graphObservations.values())
|
|
125
148
|
observation.queue.fail(error);
|
|
126
149
|
graphObservations.clear();
|
|
150
|
+
for (const observation of eventObservations.values())
|
|
151
|
+
observation.queue.fail(error);
|
|
152
|
+
eventObservations.clear();
|
|
127
153
|
};
|
|
128
154
|
const failConnection = (currentSocket, error) => {
|
|
129
155
|
if (socket !== currentSocket)
|
|
@@ -200,6 +226,12 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
200
226
|
graphObservation.queue.fail(error);
|
|
201
227
|
return;
|
|
202
228
|
}
|
|
229
|
+
const eventObservation = eventObservations.get(frame.id);
|
|
230
|
+
if (eventObservation) {
|
|
231
|
+
eventObservations.delete(frame.id);
|
|
232
|
+
eventObservation.queue.fail(error);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
203
235
|
}
|
|
204
236
|
reportError?.(error);
|
|
205
237
|
return;
|
|
@@ -238,6 +270,14 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
238
270
|
}
|
|
239
271
|
return;
|
|
240
272
|
}
|
|
273
|
+
if (frame.kind === 'event-delivery') {
|
|
274
|
+
const observation = eventObservations.get(frame.id);
|
|
275
|
+
if (!observation || frame.sequence <= observation.lastSequence)
|
|
276
|
+
return;
|
|
277
|
+
observation.lastSequence = frame.sequence;
|
|
278
|
+
observation.queue.push(frame.body);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
241
281
|
const observation = graphObservations.get(frame.id);
|
|
242
282
|
if (!observation || observation.terminal || frame.sequence <= observation.lastSequence)
|
|
243
283
|
return;
|
|
@@ -263,7 +303,7 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
263
303
|
if (connection)
|
|
264
304
|
return connection.promise;
|
|
265
305
|
if (socket) {
|
|
266
|
-
failConnection(socket, new
|
|
306
|
+
failConnection(socket, new RuntimeWebSocketSessionLostError('Runtime Protocol WebSocket session was replaced; active work was not resumed.'));
|
|
267
307
|
}
|
|
268
308
|
const currentSocket = createWebSocket(resolveWebSocketUrl(url), protocols);
|
|
269
309
|
socket = currentSocket;
|
|
@@ -274,7 +314,7 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
274
314
|
rejectConnection = reject;
|
|
275
315
|
});
|
|
276
316
|
const timeout = setTimeout(() => {
|
|
277
|
-
const error = new
|
|
317
|
+
const error = new RuntimeWebSocketSessionLostError('Runtime Protocol WebSocket session handshake timed out.');
|
|
278
318
|
failConnection(currentSocket, error);
|
|
279
319
|
currentSocket.close(1002, 'Runtime Protocol handshake timeout');
|
|
280
320
|
}, handshakeTimeoutMs);
|
|
@@ -294,8 +334,8 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
294
334
|
}
|
|
295
335
|
handleFrame(currentSocket, input);
|
|
296
336
|
};
|
|
297
|
-
const onError = () => failConnection(currentSocket, new
|
|
298
|
-
const onClose = (event) => failConnection(currentSocket, new
|
|
337
|
+
const onError = () => failConnection(currentSocket, new RuntimeWebSocketSessionLostError('Runtime Protocol WebSocket session failed.'));
|
|
338
|
+
const onClose = (event) => failConnection(currentSocket, new RuntimeWebSocketSessionLostError(`Runtime Protocol WebSocket session disconnected${event.code === undefined
|
|
299
339
|
? ''
|
|
300
340
|
: ` (${event.code}${event.reason ? `: ${event.reason}` : ''})`}; active work was not resumed.`));
|
|
301
341
|
currentSocket.addEventListener('message', onMessage);
|
|
@@ -352,32 +392,108 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
352
392
|
const observeDurable = async function* (run, options) {
|
|
353
393
|
if (options?.signal?.aborted)
|
|
354
394
|
return;
|
|
355
|
-
|
|
395
|
+
let failedAttempts = 0;
|
|
396
|
+
while (!options?.signal?.aborted) {
|
|
397
|
+
const queue = createAsyncQueue();
|
|
398
|
+
const prepared = prepareObservation(queue, options?.signal);
|
|
399
|
+
const { id } = prepared;
|
|
400
|
+
const observation = {
|
|
401
|
+
run,
|
|
402
|
+
queue,
|
|
403
|
+
lastSequence: 0,
|
|
404
|
+
terminal: false,
|
|
405
|
+
};
|
|
406
|
+
let subscribed = false;
|
|
407
|
+
try {
|
|
408
|
+
const connectedCapabilities = await awaitWithSignal(ensureConnection(), options?.signal, 'Durable Operation WebSocket observation was aborted.');
|
|
409
|
+
if (!connectedCapabilities.has('durable-operation-push')) {
|
|
410
|
+
throw new Error('Durable Operation push is unavailable in this WebSocket session.');
|
|
411
|
+
}
|
|
412
|
+
if (observationIdIsActive(id)) {
|
|
413
|
+
throw new Error(`Durable observation id ${id} is already active.`);
|
|
414
|
+
}
|
|
415
|
+
durableObservations.set(id, observation);
|
|
416
|
+
send({
|
|
417
|
+
protocol: RUNTIME_PROTOCOL_SESSION_NAME,
|
|
418
|
+
version: RUNTIME_PROTOCOL_SESSION_VERSION,
|
|
419
|
+
kind: 'durable-observe',
|
|
420
|
+
id,
|
|
421
|
+
run: { taskId: run.taskId, runId: run.runId },
|
|
422
|
+
});
|
|
423
|
+
subscribed = true;
|
|
424
|
+
while (true) {
|
|
425
|
+
const next = await queue.next();
|
|
426
|
+
if (next.done)
|
|
427
|
+
return;
|
|
428
|
+
failedAttempts = 0;
|
|
429
|
+
yield next.value;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
if (options?.signal?.aborted)
|
|
434
|
+
return;
|
|
435
|
+
if (disposed || !(error instanceof RuntimeWebSocketSessionLostError))
|
|
436
|
+
throw error;
|
|
437
|
+
failedAttempts += 1;
|
|
438
|
+
if (failedAttempts > MAX_DURABLE_OBSERVATION_RECONNECT_ATTEMPTS)
|
|
439
|
+
throw error;
|
|
440
|
+
const delay = Math.min(reconnectDelayMs * 2 ** (failedAttempts - 1), MAX_DURABLE_OBSERVATION_RECONNECT_DELAY_MS);
|
|
441
|
+
try {
|
|
442
|
+
await awaitWithSignal(new Promise(resolve => setTimeout(resolve, delay)), options?.signal, 'Durable Operation WebSocket observation was aborted.');
|
|
443
|
+
}
|
|
444
|
+
catch (delayError) {
|
|
445
|
+
if (options?.signal?.aborted)
|
|
446
|
+
return;
|
|
447
|
+
throw delayError;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
prepared.release();
|
|
452
|
+
if (durableObservations.get(id) === observation)
|
|
453
|
+
durableObservations.delete(id);
|
|
454
|
+
if (subscribed && !observation.terminal) {
|
|
455
|
+
try {
|
|
456
|
+
send({
|
|
457
|
+
protocol: RUNTIME_PROTOCOL_SESSION_NAME,
|
|
458
|
+
version: RUNTIME_PROTOCOL_SESSION_VERSION,
|
|
459
|
+
kind: 'durable-unobserve',
|
|
460
|
+
id,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// Disconnect already released the server-side session resources.
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const observeGraph = async function* (request, options) {
|
|
471
|
+
if (options?.signal?.aborted)
|
|
472
|
+
return;
|
|
356
473
|
const queue = createAsyncQueue();
|
|
474
|
+
const prepared = prepareObservation(queue, options?.signal);
|
|
475
|
+
const { id } = prepared;
|
|
357
476
|
const observation = {
|
|
358
|
-
run,
|
|
359
477
|
queue,
|
|
360
478
|
lastSequence: 0,
|
|
361
479
|
terminal: false,
|
|
362
480
|
};
|
|
363
481
|
let subscribed = false;
|
|
364
|
-
const abort = () => queue.end();
|
|
365
|
-
options?.signal?.addEventListener('abort', abort, { once: true });
|
|
366
482
|
try {
|
|
367
|
-
const connectedCapabilities = await awaitWithSignal(ensureConnection(), options?.signal, '
|
|
368
|
-
if (!connectedCapabilities.has('
|
|
369
|
-
throw new Error('
|
|
483
|
+
const connectedCapabilities = await awaitWithSignal(ensureConnection(), options?.signal, 'Data graph WebSocket observation was aborted.');
|
|
484
|
+
if (!connectedCapabilities.has('graph-observation-push')) {
|
|
485
|
+
throw new Error('Data graph push observation is unavailable in this WebSocket session.');
|
|
370
486
|
}
|
|
371
|
-
if (
|
|
372
|
-
throw new Error(`
|
|
487
|
+
if (observationIdIsActive(id)) {
|
|
488
|
+
throw new Error(`Graph observation id ${id} is already active.`);
|
|
373
489
|
}
|
|
374
|
-
|
|
490
|
+
graphObservations.set(id, observation);
|
|
375
491
|
send({
|
|
376
492
|
protocol: RUNTIME_PROTOCOL_SESSION_NAME,
|
|
377
493
|
version: RUNTIME_PROTOCOL_SESSION_VERSION,
|
|
378
|
-
kind: '
|
|
494
|
+
kind: 'graph-observe',
|
|
379
495
|
id,
|
|
380
|
-
|
|
496
|
+
request,
|
|
381
497
|
});
|
|
382
498
|
subscribed = true;
|
|
383
499
|
while (true) {
|
|
@@ -392,15 +508,15 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
392
508
|
throw error;
|
|
393
509
|
}
|
|
394
510
|
finally {
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
511
|
+
prepared.release();
|
|
512
|
+
if (graphObservations.get(id) === observation)
|
|
513
|
+
graphObservations.delete(id);
|
|
398
514
|
if (subscribed && !observation.terminal) {
|
|
399
515
|
try {
|
|
400
516
|
send({
|
|
401
517
|
protocol: RUNTIME_PROTOCOL_SESSION_NAME,
|
|
402
518
|
version: RUNTIME_PROTOCOL_SESSION_VERSION,
|
|
403
|
-
kind: '
|
|
519
|
+
kind: 'graph-unobserve',
|
|
404
520
|
id,
|
|
405
521
|
});
|
|
406
522
|
}
|
|
@@ -410,34 +526,30 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
410
526
|
}
|
|
411
527
|
}
|
|
412
528
|
};
|
|
413
|
-
const
|
|
529
|
+
const observeEvent = async function* (event, options) {
|
|
414
530
|
if (options?.signal?.aborted)
|
|
415
531
|
return;
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
lastSequence: 0,
|
|
421
|
-
terminal: false,
|
|
422
|
-
};
|
|
532
|
+
const queue = createAsyncQueue(256);
|
|
533
|
+
const prepared = prepareObservation(queue, options?.signal);
|
|
534
|
+
const { id } = prepared;
|
|
535
|
+
const observation = { queue, lastSequence: 0 };
|
|
423
536
|
let subscribed = false;
|
|
424
|
-
const abort = () => queue.end();
|
|
425
|
-
options?.signal?.addEventListener('abort', abort, { once: true });
|
|
426
537
|
try {
|
|
427
|
-
const connectedCapabilities = await awaitWithSignal(ensureConnection(), options?.signal, '
|
|
428
|
-
if (!connectedCapabilities.has('
|
|
429
|
-
throw new Error('
|
|
538
|
+
const connectedCapabilities = await awaitWithSignal(ensureConnection(), options?.signal, 'Event WebSocket observation was aborted.');
|
|
539
|
+
if (!connectedCapabilities.has('event-subscription-push')) {
|
|
540
|
+
throw new Error('Event push subscription is unavailable in this WebSocket session.');
|
|
430
541
|
}
|
|
431
|
-
if (
|
|
432
|
-
throw new Error(`
|
|
542
|
+
if (observationIdIsActive(id)) {
|
|
543
|
+
throw new Error(`Event observation id ${id} is already active.`);
|
|
433
544
|
}
|
|
434
|
-
|
|
545
|
+
eventObservations.set(id, observation);
|
|
435
546
|
send({
|
|
436
547
|
protocol: RUNTIME_PROTOCOL_SESSION_NAME,
|
|
437
548
|
version: RUNTIME_PROTOCOL_SESSION_VERSION,
|
|
438
|
-
kind: '
|
|
549
|
+
kind: 'event-observe',
|
|
439
550
|
id,
|
|
440
|
-
|
|
551
|
+
eventId: event.id,
|
|
552
|
+
eventVersion: event.version,
|
|
441
553
|
});
|
|
442
554
|
subscribed = true;
|
|
443
555
|
while (true) {
|
|
@@ -452,28 +564,30 @@ export const createWebSocketRuntimeTransport = ({ url, protocols, createWebSocke
|
|
|
452
564
|
throw error;
|
|
453
565
|
}
|
|
454
566
|
finally {
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
if (subscribed
|
|
567
|
+
prepared.release();
|
|
568
|
+
if (eventObservations.get(id) === observation)
|
|
569
|
+
eventObservations.delete(id);
|
|
570
|
+
if (subscribed) {
|
|
459
571
|
try {
|
|
460
572
|
send({
|
|
461
573
|
protocol: RUNTIME_PROTOCOL_SESSION_NAME,
|
|
462
574
|
version: RUNTIME_PROTOCOL_SESSION_VERSION,
|
|
463
|
-
kind: '
|
|
575
|
+
kind: 'event-unobserve',
|
|
464
576
|
id,
|
|
465
577
|
});
|
|
466
578
|
}
|
|
467
579
|
catch {
|
|
468
|
-
// Disconnect already released the server-side
|
|
580
|
+
// Disconnect already released the server-side subscription.
|
|
469
581
|
}
|
|
470
582
|
}
|
|
471
583
|
}
|
|
472
584
|
};
|
|
473
585
|
return {
|
|
586
|
+
descriptor: { kind: 'websocket' },
|
|
474
587
|
request,
|
|
475
588
|
durableOperation: { observe: observeDurable },
|
|
476
589
|
graph: { observe: observeGraph },
|
|
590
|
+
events: { observe: observeEvent },
|
|
477
591
|
close: () => {
|
|
478
592
|
if (disposed)
|
|
479
593
|
return;
|