@jigs.design/client 0.2.0 → 0.3.1
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 +2 -2
- package/dist/client.d.ts +54 -5
- package/dist/client.js +196 -4
- package/dist/index.d.ts +3 -3
- package/dist/transport.d.ts +25 -5
- package/dist/transport.js +182 -50
- package/dist/view/controller.d.ts +7 -1
- package/dist/view/controller.js +57 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@ import { createJigsClient } from "@jigs.design/client";
|
|
|
8
8
|
export const client = createJigsClient({ endpoint: "/api/jigs" });
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Create one client per app, in its own module. Every view and session that imports it shares one query cache, so two views reading the same query make one request, and a mutation's invalidations reach both. The client retries network failures, aborts a query's request when nothing reads it any more, and refreshes the queries a mutation invalidates.
|
|
11
|
+
Create one client per app, in its own module. Every view and session that imports it shares one query cache, so two views reading the same query make one request, and a mutation's invalidations reach both. Pages' programs sit in a second cache, keyed by name and props, so a page and a query may share a name. The client retries network failures, aborts a query's request when nothing reads it any more, and refreshes the queries a mutation invalidates. When the server has `events`, it opens the push stream while anything is observed and refetches the queries and pages an event names.
|
|
12
12
|
|
|
13
13
|
## Exports
|
|
14
14
|
|
|
15
15
|
| Export | What it is |
|
|
16
16
|
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
17
|
-
| `createJigsClient` | The client, with `observe`, `mutate`, `invalidate` and `
|
|
17
|
+
| `createJigsClient` | The client, with `observe`, `mutate`, `invalidate`, `session`, and `page`, `observePage` and `invalidatePage` for pages |
|
|
18
18
|
| `httpTransport`, `Transport` | The default HTTP transport, and the type to implement your own |
|
|
19
19
|
| `TransportError` | What a transport rejects with: `{ code, message }` |
|
|
20
20
|
| `createViewController`, `ViewController` | One view's page side: `start`, `load`, `subscribe`, `store`, `drawnProps`, `call`, `stop`, and the handlers for issues, actions and confirmation |
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HeadersOption, Transport } from "./transport.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Diagnostic, JigsErrorData, MutationResult, PageProps, Program, QueryResult, SessionBody, SessionEvent } from "@jigs.design/protocol";
|
|
3
3
|
//#region src/client.d.ts
|
|
4
4
|
interface CacheOptions {
|
|
5
5
|
/**
|
|
@@ -46,11 +46,46 @@ export type ClientOptions = CacheOptions & ConfirmOptions & ({
|
|
|
46
46
|
headers?: never;
|
|
47
47
|
transport: Transport;
|
|
48
48
|
});
|
|
49
|
+
/** A page's program and its diagnostics, as the server compiled them. */
|
|
50
|
+
export interface PageData {
|
|
51
|
+
/**
|
|
52
|
+
* A saved page is compiled leniently, so these may hold errors: show a
|
|
53
|
+
* notice beside the program when one is an error or a warning.
|
|
54
|
+
*/
|
|
55
|
+
diagnostics: ReadonlyArray<Diagnostic>;
|
|
56
|
+
program: Program;
|
|
57
|
+
}
|
|
58
|
+
/** A page as `observePage` reports it. */
|
|
59
|
+
export interface PageResult {
|
|
60
|
+
/** The program's diagnostics; empty until one arrives. */
|
|
61
|
+
diagnostics: ReadonlyArray<Diagnostic>;
|
|
62
|
+
/** `{ code, message }` when `status` is "error". */
|
|
63
|
+
error: JigsErrorData | null;
|
|
64
|
+
/**
|
|
65
|
+
* The latest program, or null before the first one. A refetch that fails
|
|
66
|
+
* keeps the last program here, beside the error.
|
|
67
|
+
*/
|
|
68
|
+
program: Program | null;
|
|
69
|
+
status: "error" | "pending" | "success";
|
|
70
|
+
}
|
|
49
71
|
export interface JigsClient {
|
|
50
72
|
/** URL of the sandbox frame document. */
|
|
51
73
|
readonly frame: string;
|
|
74
|
+
/**
|
|
75
|
+
* Keeps the push channel from closing until the returned function is
|
|
76
|
+
* called; it does not open one. A view holds it from `start` to `stop`,
|
|
77
|
+
* so a program that reads no query for a while, as one still streaming
|
|
78
|
+
* may, does not close and reopen the stream.
|
|
79
|
+
*/
|
|
80
|
+
hold: () => () => void;
|
|
52
81
|
/** Refetches every query with this name, for every input. */
|
|
53
82
|
invalidate: (name: string) => Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Drops the cached program of page `name`: of every props, or of the
|
|
85
|
+
* props that contain `props`, as a query's input matches. Pages on
|
|
86
|
+
* screen refetch; the rest fetch when next observed.
|
|
87
|
+
*/
|
|
88
|
+
invalidatePage: (name: string, props?: PageProps) => Promise<void>;
|
|
54
89
|
/**
|
|
55
90
|
* Runs one mutation or action and, when it succeeds, refetches the
|
|
56
91
|
* queries the server says it refreshed. A reply without that list
|
|
@@ -74,11 +109,25 @@ export interface JigsClient {
|
|
|
74
109
|
* `kind: "query"`.
|
|
75
110
|
*/
|
|
76
111
|
observe: (name: string, input: unknown, listener: (result: QueryResult) => void) => () => void;
|
|
112
|
+
/**
|
|
113
|
+
* Keeps one page subscribed and reports every result, as `observe` does
|
|
114
|
+
* for a query. `initial` is a program the page already has, such as one a
|
|
115
|
+
* loader rendered: it fills the cache, so it is not fetched again on
|
|
116
|
+
* mount. Pages are cached by name and props until `invalidatePage` or a
|
|
117
|
+
* reply from a server of another version.
|
|
118
|
+
*/
|
|
119
|
+
observePage: (name: string, props: PageProps | undefined, listener: (result: PageResult) => void, options?: {
|
|
120
|
+
initial?: PageData | undefined;
|
|
121
|
+
}) => () => void;
|
|
122
|
+
/**
|
|
123
|
+
* A page's program by name, from the cache when it holds one, else
|
|
124
|
+
* through `POST { type: "page" }`. Rejects with the transport's error.
|
|
125
|
+
*/
|
|
126
|
+
page: (name: string, props?: PageProps, options?: {
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}) => Promise<PageData>;
|
|
77
129
|
/** One turn, as session events. */
|
|
78
|
-
session: (body: {
|
|
79
|
-
messages: ReadonlyArray<Message>;
|
|
80
|
-
options?: SessionOptions;
|
|
81
|
-
}, options: {
|
|
130
|
+
session: (body: SessionBody, options: {
|
|
82
131
|
signal: AbortSignal;
|
|
83
132
|
}) => AsyncIterable<SessionEvent>;
|
|
84
133
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,10 +1,95 @@
|
|
|
1
1
|
import { TransportError, errorDataOf, httpTransport } from "./transport.js";
|
|
2
|
-
import { QueryClient, QueryObserver } from "@tanstack/query-core";
|
|
2
|
+
import { QueryClient, QueryObserver, hashKey } from "@tanstack/query-core";
|
|
3
3
|
//#region src/client.ts
|
|
4
4
|
/** How long a query result stays fresh by default: a minute. */
|
|
5
5
|
const DEFAULT_STALE_TIME = 6e4;
|
|
6
6
|
const RETRYABLE = /* @__PURE__ */ new Set(["NETWORK", "UNAVAILABLE"]);
|
|
7
7
|
const MAX_RETRIES = 2;
|
|
8
|
+
/**
|
|
9
|
+
* How long the push channel stays open once nothing observes or holds it:
|
|
10
|
+
* a view that remounts, or a block that becomes another, subscribes again
|
|
11
|
+
* well within it.
|
|
12
|
+
*/
|
|
13
|
+
const CHANNEL_LINGER_MS = 1e3;
|
|
14
|
+
/** When a channel the server refused or ended is tried again, with queries still observed. */
|
|
15
|
+
const CHANNEL_REOPEN_MS = 3e5;
|
|
16
|
+
/** The refetch after a gap in the channel waits up to this long, so tabs do not all hit the server at once. */
|
|
17
|
+
const GAP_SPREAD_MS = 2e3;
|
|
18
|
+
/**
|
|
19
|
+
* The push channel: opened by the first query or page observed, kept open
|
|
20
|
+
* while any is observed or a view holds it, and its events applied to the
|
|
21
|
+
* cache. It refetches everything on screen only after a gap
|
|
22
|
+
* it cannot account for: a connection lost without a `close` event, to a
|
|
23
|
+
* server that did not replay what it missed.
|
|
24
|
+
*/
|
|
25
|
+
function createChannel(transport, handlers) {
|
|
26
|
+
let observers = 0;
|
|
27
|
+
/** Views mounted: they keep an open channel open, but do not open one. */
|
|
28
|
+
let holds = 0;
|
|
29
|
+
let running = null;
|
|
30
|
+
let linger;
|
|
31
|
+
let reopen;
|
|
32
|
+
const consume = async (events, signal) => {
|
|
33
|
+
let first = true;
|
|
34
|
+
let closed = false;
|
|
35
|
+
try {
|
|
36
|
+
for await (const event of events) if (event.type === "invalidate") handlers.apply(event.invalidate);
|
|
37
|
+
else if (event.type === "invalidate-pages") handlers.applyPages(event.pages);
|
|
38
|
+
else if (event.type === "close") closed = true;
|
|
39
|
+
else if (event.type === "open") {
|
|
40
|
+
if (!first && !closed && !event.replayed) setTimeout(handlers.refetchActive, Math.random() * GAP_SPREAD_MS);
|
|
41
|
+
first = false;
|
|
42
|
+
closed = false;
|
|
43
|
+
} else if (event.type === "version") handlers.version(event.version);
|
|
44
|
+
} catch {}
|
|
45
|
+
if (signal.aborted) return;
|
|
46
|
+
running = null;
|
|
47
|
+
clearTimeout(reopen);
|
|
48
|
+
reopen = setTimeout(() => {
|
|
49
|
+
if (observers > 0 && running === null) open();
|
|
50
|
+
}, CHANNEL_REOPEN_MS);
|
|
51
|
+
};
|
|
52
|
+
const open = () => {
|
|
53
|
+
if (transport.events === void 0) return;
|
|
54
|
+
clearTimeout(reopen);
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
running = controller;
|
|
57
|
+
consume(transport.events({ signal: controller.signal }), controller.signal);
|
|
58
|
+
};
|
|
59
|
+
/** Closes the channel after the linger, once nothing observes or holds it. */
|
|
60
|
+
const closeWhenIdle = () => {
|
|
61
|
+
if (observers > 0 || holds > 0) return;
|
|
62
|
+
clearTimeout(linger);
|
|
63
|
+
linger = setTimeout(() => {
|
|
64
|
+
clearTimeout(reopen);
|
|
65
|
+
running?.abort();
|
|
66
|
+
running = null;
|
|
67
|
+
}, CHANNEL_LINGER_MS);
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
hold: () => {
|
|
71
|
+
holds += 1;
|
|
72
|
+
clearTimeout(linger);
|
|
73
|
+
let held = true;
|
|
74
|
+
return () => {
|
|
75
|
+
if (held) {
|
|
76
|
+
held = false;
|
|
77
|
+
holds -= 1;
|
|
78
|
+
closeWhenIdle();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
release: () => {
|
|
83
|
+
observers -= 1;
|
|
84
|
+
closeWhenIdle();
|
|
85
|
+
},
|
|
86
|
+
retain: () => {
|
|
87
|
+
observers += 1;
|
|
88
|
+
clearTimeout(linger);
|
|
89
|
+
if (running === null) open();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
8
93
|
function toQueryResult(result) {
|
|
9
94
|
if (result.status === "success") return {
|
|
10
95
|
data: result.data,
|
|
@@ -22,6 +107,18 @@ function toQueryResult(result) {
|
|
|
22
107
|
status: "pending"
|
|
23
108
|
};
|
|
24
109
|
}
|
|
110
|
+
/** A page's key in the program cache: its name, then its props. */
|
|
111
|
+
function pageKey(name, props) {
|
|
112
|
+
return [name, props ?? {}];
|
|
113
|
+
}
|
|
114
|
+
function toPageResult(result) {
|
|
115
|
+
return {
|
|
116
|
+
diagnostics: result.data?.diagnostics ?? [],
|
|
117
|
+
error: result.status === "error" ? errorDataOf(result.error) : null,
|
|
118
|
+
program: result.data?.program ?? null,
|
|
119
|
+
status: result.status
|
|
120
|
+
};
|
|
121
|
+
}
|
|
25
122
|
/**
|
|
26
123
|
* The browser's connection to a Jigs server. Create one per app, in its
|
|
27
124
|
* own module: each client has its own query cache, so two clients share no
|
|
@@ -39,9 +136,61 @@ function createJigsClient(options) {
|
|
|
39
136
|
staleTime: options.staleTime ?? DEFAULT_STALE_TIME
|
|
40
137
|
}
|
|
41
138
|
} });
|
|
139
|
+
const applyInvalidations = (targets) => {
|
|
140
|
+
for (const target of targets) queries.invalidateQueries({ queryKey: target.input === void 0 ? [target.name] : [target.name, target.input] });
|
|
141
|
+
};
|
|
142
|
+
const programs = new QueryClient({ defaultOptions: { queries: {
|
|
143
|
+
gcTime: 3e5,
|
|
144
|
+
refetchOnReconnect: false,
|
|
145
|
+
refetchOnWindowFocus: false,
|
|
146
|
+
retry: (failures, error) => failures < MAX_RETRIES && RETRYABLE.has(errorDataOf(error).code),
|
|
147
|
+
staleTime: Number.POSITIVE_INFINITY
|
|
148
|
+
} } });
|
|
149
|
+
/** The server version the cached programs came from, once one is known. */
|
|
150
|
+
let programsVersion = null;
|
|
151
|
+
/**
|
|
152
|
+
* Notes the server's version. Programs a server of another version
|
|
153
|
+
* compiled are dropped, except `own`'s, the reply that told us: pages on
|
|
154
|
+
* screen refetch, and the rest go.
|
|
155
|
+
*/
|
|
156
|
+
const noteVersion = (version, own) => {
|
|
157
|
+
const previous = programsVersion;
|
|
158
|
+
programsVersion = version;
|
|
159
|
+
if (previous === null || previous === version) return;
|
|
160
|
+
const ownHash = own === void 0 ? null : hashKey(own);
|
|
161
|
+
const others = (query) => query.queryHash !== ownHash;
|
|
162
|
+
programs.removeQueries({ predicate: (query) => others(query) && query.getObserversCount() === 0 });
|
|
163
|
+
programs.invalidateQueries({ predicate: others });
|
|
164
|
+
};
|
|
165
|
+
const fetchPage = (name, props) => async (context) => {
|
|
166
|
+
const reply = await transport.page(props === void 0 || Object.keys(props).length === 0 ? { name } : {
|
|
167
|
+
name,
|
|
168
|
+
props
|
|
169
|
+
}, { signal: context.signal });
|
|
170
|
+
noteVersion(reply.version, pageKey(name, props));
|
|
171
|
+
return {
|
|
172
|
+
diagnostics: reply.diagnostics,
|
|
173
|
+
program: reply.program
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/** Pages by name, or by name and the props they contain, as queries match. */
|
|
177
|
+
const invalidatePage = (name, props) => programs.invalidateQueries({ queryKey: props === void 0 ? [name] : [name, props] });
|
|
178
|
+
const channel = createChannel(transport, {
|
|
179
|
+
apply: applyInvalidations,
|
|
180
|
+
applyPages: (pages) => {
|
|
181
|
+
for (const page of pages) invalidatePage(page.name, page.props);
|
|
182
|
+
},
|
|
183
|
+
refetchActive: () => {
|
|
184
|
+
queries.invalidateQueries({ type: "active" });
|
|
185
|
+
programs.invalidateQueries({ type: "active" });
|
|
186
|
+
},
|
|
187
|
+
version: (version) => noteVersion(version)
|
|
188
|
+
});
|
|
42
189
|
return {
|
|
43
190
|
frame: transport.frame,
|
|
191
|
+
hold: channel.hold,
|
|
44
192
|
invalidate: (name) => queries.invalidateQueries({ queryKey: [name] }),
|
|
193
|
+
invalidatePage,
|
|
45
194
|
mutate: async (name, input, procedure) => {
|
|
46
195
|
const send = async (confirmation) => {
|
|
47
196
|
try {
|
|
@@ -81,8 +230,7 @@ function createJigsClient(options) {
|
|
|
81
230
|
error: unconfirmed,
|
|
82
231
|
status: "error"
|
|
83
232
|
};
|
|
84
|
-
|
|
85
|
-
for (const target of targets) queries.invalidateQueries({ queryKey: target.input === void 0 ? [target.name] : [target.name, target.input] });
|
|
233
|
+
applyInvalidations(reply.invalidate ?? procedure.invalidates.map((target) => ({ name: target })));
|
|
86
234
|
return {
|
|
87
235
|
data: reply.data,
|
|
88
236
|
status: "success"
|
|
@@ -104,8 +252,52 @@ function createJigsClient(options) {
|
|
|
104
252
|
const unsubscribe = observer.subscribe((result) => {
|
|
105
253
|
listener(toQueryResult(result));
|
|
106
254
|
});
|
|
255
|
+
channel.retain();
|
|
107
256
|
listener(toQueryResult(observer.getCurrentResult()));
|
|
108
|
-
|
|
257
|
+
let subscribed = true;
|
|
258
|
+
return () => {
|
|
259
|
+
if (!subscribed) return;
|
|
260
|
+
subscribed = false;
|
|
261
|
+
unsubscribe();
|
|
262
|
+
channel.release();
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
observePage: (name, props, listener, observeOptions) => {
|
|
266
|
+
const initial = observeOptions?.initial;
|
|
267
|
+
const pageOptions = {
|
|
268
|
+
queryFn: fetchPage(name, props),
|
|
269
|
+
queryKey: pageKey(name, props)
|
|
270
|
+
};
|
|
271
|
+
if (initial !== void 0) {
|
|
272
|
+
pageOptions.initialData = initial;
|
|
273
|
+
pageOptions.initialDataUpdatedAt = Date.now();
|
|
274
|
+
}
|
|
275
|
+
const observer = new QueryObserver(programs, pageOptions);
|
|
276
|
+
const unsubscribe = observer.subscribe((result) => {
|
|
277
|
+
listener(toPageResult(result));
|
|
278
|
+
});
|
|
279
|
+
channel.retain();
|
|
280
|
+
listener(toPageResult(observer.getCurrentResult()));
|
|
281
|
+
let subscribed = true;
|
|
282
|
+
return () => {
|
|
283
|
+
if (!subscribed) return;
|
|
284
|
+
subscribed = false;
|
|
285
|
+
unsubscribe();
|
|
286
|
+
channel.release();
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
page: (name, props, pageOptions) => {
|
|
290
|
+
const fetched = programs.query({
|
|
291
|
+
queryFn: fetchPage(name, props),
|
|
292
|
+
queryKey: pageKey(name, props)
|
|
293
|
+
});
|
|
294
|
+
const signal = pageOptions?.signal;
|
|
295
|
+
if (signal === void 0) return fetched;
|
|
296
|
+
return Promise.race([fetched, new Promise((_resolve, reject) => {
|
|
297
|
+
const abort = () => reject(signal.reason);
|
|
298
|
+
if (signal.aborted) abort();
|
|
299
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
300
|
+
})]);
|
|
109
301
|
},
|
|
110
302
|
session: (body, sessionOptions) => transport.session(body, sessionOptions)
|
|
111
303
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CallReply, HeadersOption, HttpTransportOptions, Transport, TransportError, httpTransport } from "./transport.js";
|
|
2
|
-
import { ClientOptions, ConfirmFunction, JigsClient, createJigsClient } from "./client.js";
|
|
2
|
+
import { ClientOptions, ConfirmFunction, JigsClient, PageData, PageResult, createJigsClient } from "./client.js";
|
|
3
3
|
import { ActionCall, ActionHandler, ViewController, ViewIssue, ViewStatus, createViewController, missingComponents } from "./view/controller.js";
|
|
4
|
-
import { Diagnostic, Invalidation, JigsErrorCode, JigsErrorData, Message, Program, SessionEvent, Severity, Span } from "@jigs.design/protocol";
|
|
5
|
-
export { type ActionCall, type ActionHandler, type CallReply, type ClientOptions, type ConfirmFunction, type Diagnostic, type HeadersOption, type HttpTransportOptions, type Invalidation, type JigsClient, type JigsErrorCode, type JigsErrorData, type Message, type Program, type SessionEvent, type Severity, type Span, type Transport, TransportError, type ViewController, type ViewIssue, type ViewStatus, createJigsClient, createViewController, httpTransport, missingComponents };
|
|
4
|
+
import { Diagnostic, Invalidation, JigsErrorCode, JigsErrorData, Message, PageProps, PageReply, Program, PushEvent, SessionBody, SessionEvent, Severity, Span } from "@jigs.design/protocol";
|
|
5
|
+
export { type ActionCall, type ActionHandler, type CallReply, type ClientOptions, type ConfirmFunction, type Diagnostic, type HeadersOption, type HttpTransportOptions, type Invalidation, type JigsClient, type JigsErrorCode, type JigsErrorData, type Message, type PageData, type PageProps, type PageReply, type PageResult, type Program, type PushEvent, type SessionBody, type SessionEvent, type Severity, type Span, type Transport, TransportError, type ViewController, type ViewIssue, type ViewStatus, createJigsClient, createViewController, httpTransport, missingComponents };
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Invalidation, JigsErrorCode, JigsErrorData,
|
|
1
|
+
import { Invalidation, JigsErrorCode, JigsErrorData, PageProps, PageReply, ProcedureKind, PushEvent, SessionBody, SessionEvent } from "@jigs.design/protocol";
|
|
2
2
|
//#region src/transport.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* A call's reply: the procedure's data and, for a mutation, the queries the
|
|
@@ -32,14 +32,34 @@ export interface Transport {
|
|
|
32
32
|
}, options: {
|
|
33
33
|
signal: AbortSignal;
|
|
34
34
|
}) => Promise<CallReply>;
|
|
35
|
+
/**
|
|
36
|
+
* Opens the push channel and yields its events until `signal` aborts.
|
|
37
|
+
* The transport owns the connection: it reconnects with backoff, resends
|
|
38
|
+
* the last event id, and yields `open` each time a connection is made
|
|
39
|
+
* (then `version`, when it knows the server's). It ends without an abort
|
|
40
|
+
* when the server has no channel or refuses it; the client asks again
|
|
41
|
+
* later. Never throws. Without it, the client has no push.
|
|
42
|
+
*/
|
|
43
|
+
events?: (options: {
|
|
44
|
+
signal: AbortSignal;
|
|
45
|
+
}) => AsyncIterable<PushEvent>;
|
|
35
46
|
/** URL of the sandbox frame document. */
|
|
36
47
|
frame: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
/**
|
|
49
|
+
* A page's compiled program by name, with its diagnostics and the
|
|
50
|
+
* server's version. Rejects with a `TransportError`: `NOT_FOUND` for a
|
|
51
|
+
* page the server will not serve, `BAD_INPUT` for props it refuses. A
|
|
52
|
+
* transport that cannot fetch pages rejects with `UNAVAILABLE`.
|
|
53
|
+
*/
|
|
54
|
+
page: (request: {
|
|
55
|
+
name: string;
|
|
56
|
+
props?: PageProps;
|
|
41
57
|
}, options: {
|
|
42
58
|
signal: AbortSignal;
|
|
59
|
+
}) => Promise<PageReply>;
|
|
60
|
+
/** Reports every failure as one `error` event and then ends; never throws. */
|
|
61
|
+
session: (body: SessionBody, options: {
|
|
62
|
+
signal: AbortSignal;
|
|
43
63
|
}) => AsyncIterable<SessionEvent>;
|
|
44
64
|
}
|
|
45
65
|
/** A failure with a code the spec can read. */
|
package/dist/transport.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseErrorData, parseInvalidations, parseSessionEvent } from "@jigs.design/protocol";
|
|
1
|
+
import { parseErrorData, parseInvalidations, parsePageReply, parsePushEvent, parseSessionEvent } from "@jigs.design/protocol";
|
|
2
2
|
//#region src/transport.ts
|
|
3
3
|
/** A failure with a code the spec can read. */
|
|
4
4
|
var TransportError = class extends Error {
|
|
@@ -15,6 +15,74 @@ const GATEWAY_STATUSES = /* @__PURE__ */ new Set([
|
|
|
15
15
|
503,
|
|
16
16
|
504
|
|
17
17
|
]);
|
|
18
|
+
/**
|
|
19
|
+
* Push stream answers a reconnect would not change: no channel (204), a
|
|
20
|
+
* refusal (401, 403), or a server that does not know the route (404, 405).
|
|
21
|
+
*/
|
|
22
|
+
const FINAL_STREAM_STATUSES = /* @__PURE__ */ new Set([
|
|
23
|
+
204,
|
|
24
|
+
401,
|
|
25
|
+
403,
|
|
26
|
+
404,
|
|
27
|
+
405
|
|
28
|
+
]);
|
|
29
|
+
/** The reconnect delay until the server sends `retry:`, and the most it doubles to. */
|
|
30
|
+
const RETRY_MS = 3e3;
|
|
31
|
+
const MAX_RETRY_MS = 3e4;
|
|
32
|
+
/** A connection that lived this long resets the doubling. */
|
|
33
|
+
const STABLE_MS = 1e4;
|
|
34
|
+
/**
|
|
35
|
+
* The frames of a server-sent event stream, as they complete. Ends when
|
|
36
|
+
* `signal` aborts, even if the body does not.
|
|
37
|
+
*/
|
|
38
|
+
async function* sseFrames(body, signal) {
|
|
39
|
+
const reader = body.pipeThrough(new TextDecoderStream()).getReader();
|
|
40
|
+
const stop = () => {
|
|
41
|
+
reader.cancel().catch(() => void 0);
|
|
42
|
+
};
|
|
43
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
44
|
+
let buffer = "";
|
|
45
|
+
try {
|
|
46
|
+
for (;;) {
|
|
47
|
+
const chunk = await reader.read();
|
|
48
|
+
if (chunk.done) return;
|
|
49
|
+
buffer += chunk.value;
|
|
50
|
+
let end = buffer.indexOf("\n\n");
|
|
51
|
+
while (end !== -1) {
|
|
52
|
+
const lines = buffer.slice(0, end).split("\n");
|
|
53
|
+
buffer = buffer.slice(end + 2);
|
|
54
|
+
const field = (name) => lines.filter((line) => line.startsWith(`${name}:`)).map((line) => line.slice(name.length + 1).trimStart());
|
|
55
|
+
const retry = Number(field("retry").at(-1) ?? "NaN");
|
|
56
|
+
yield {
|
|
57
|
+
data: field("data").join("\n"),
|
|
58
|
+
id: field("id").at(-1) ?? null,
|
|
59
|
+
retry: Number.isInteger(retry) && retry >= 0 ? retry : null
|
|
60
|
+
};
|
|
61
|
+
end = buffer.indexOf("\n\n");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
signal.removeEventListener("abort", stop);
|
|
66
|
+
await reader.cancel().catch(() => void 0);
|
|
67
|
+
reader.releaseLock();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Waits `ms`, or less when `signal` aborts. */
|
|
71
|
+
function pause(ms, signal) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
if (signal.aborted) {
|
|
74
|
+
resolve();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const done = () => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
signal.removeEventListener("abort", done);
|
|
80
|
+
resolve();
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(done, ms);
|
|
83
|
+
signal.addEventListener("abort", done, { once: true });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
18
86
|
const unavailable = {
|
|
19
87
|
code: "UNAVAILABLE",
|
|
20
88
|
message: "The server is not available right now."
|
|
@@ -33,6 +101,17 @@ async function replyError(response) {
|
|
|
33
101
|
return unavailable;
|
|
34
102
|
}
|
|
35
103
|
}
|
|
104
|
+
/** A JSON reply's body, or the `TransportError` it carries. */
|
|
105
|
+
async function replyBody(response) {
|
|
106
|
+
if (!response.ok || !response.headers.has(VERSION_HEADER)) throw new TransportError(await replyError(response));
|
|
107
|
+
let body;
|
|
108
|
+
try {
|
|
109
|
+
body = await response.json();
|
|
110
|
+
} catch {
|
|
111
|
+
throw new TransportError(unavailable);
|
|
112
|
+
}
|
|
113
|
+
return typeof body === "object" ? body : null;
|
|
114
|
+
}
|
|
36
115
|
/** Why the `headers` function failed, as the call's error. */
|
|
37
116
|
function headersError(cause) {
|
|
38
117
|
return thrownErrorData(cause) ?? {
|
|
@@ -49,7 +128,7 @@ function httpTransport(options) {
|
|
|
49
128
|
* token refresh that failed, is not a network failure: it keeps a thrown
|
|
50
129
|
* `{ code, message }` and is otherwise `UNAUTHORIZED`, never retried.
|
|
51
130
|
*/
|
|
52
|
-
const headers = async () => {
|
|
131
|
+
const headers = async (contentType = "application/json") => {
|
|
53
132
|
let extra;
|
|
54
133
|
try {
|
|
55
134
|
extra = typeof options.headers === "function" ? await options.headers() : options.headers;
|
|
@@ -57,7 +136,7 @@ function httpTransport(options) {
|
|
|
57
136
|
throw new TransportError(headersError(error));
|
|
58
137
|
}
|
|
59
138
|
const merged = new Headers(extra);
|
|
60
|
-
merged.set("content-type",
|
|
139
|
+
if (contentType !== null) merged.set("content-type", contentType);
|
|
61
140
|
return merged;
|
|
62
141
|
};
|
|
63
142
|
const post = (body, init, signal) => fetcher(options.endpoint, {
|
|
@@ -67,30 +146,26 @@ function httpTransport(options) {
|
|
|
67
146
|
method: "POST",
|
|
68
147
|
signal
|
|
69
148
|
});
|
|
149
|
+
/** Posts `body`; a fetch that fails without an abort is `NETWORK`. */
|
|
150
|
+
const send = async (body, signal) => {
|
|
151
|
+
const init = await headers();
|
|
152
|
+
try {
|
|
153
|
+
return await post(body, init, signal);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (signal.aborted) throw error;
|
|
156
|
+
throw new TransportError(network);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
70
159
|
return {
|
|
71
160
|
async call(request, callOptions) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
type: "call"
|
|
81
|
-
}), init, callOptions.signal);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (callOptions.signal.aborted) throw error;
|
|
84
|
-
throw new TransportError(network);
|
|
85
|
-
}
|
|
86
|
-
if (!response.ok || !response.headers.has(VERSION_HEADER)) throw new TransportError(await replyError(response));
|
|
87
|
-
let body;
|
|
88
|
-
try {
|
|
89
|
-
body = await response.json();
|
|
90
|
-
} catch {
|
|
91
|
-
throw new TransportError(unavailable);
|
|
92
|
-
}
|
|
93
|
-
if (typeof body !== "object" || body === null) throw new TransportError(unavailable);
|
|
161
|
+
const body = await replyBody(await send(JSON.stringify({
|
|
162
|
+
confirmation: request.confirmation,
|
|
163
|
+
input: request.input,
|
|
164
|
+
kind: request.kind,
|
|
165
|
+
name: request.name,
|
|
166
|
+
type: "call"
|
|
167
|
+
}), callOptions.signal));
|
|
168
|
+
if (body === null) throw new TransportError(unavailable);
|
|
94
169
|
if ("requiresAction" in body) {
|
|
95
170
|
const requiresAction = parseRequiresAction(body.requiresAction);
|
|
96
171
|
if (requiresAction === null) throw new TransportError(unavailable);
|
|
@@ -105,7 +180,79 @@ function httpTransport(options) {
|
|
|
105
180
|
invalidate
|
|
106
181
|
};
|
|
107
182
|
},
|
|
183
|
+
async *events({ signal }) {
|
|
184
|
+
let lastId = null;
|
|
185
|
+
let retryMs = RETRY_MS;
|
|
186
|
+
let failures = 0;
|
|
187
|
+
while (!signal.aborted) {
|
|
188
|
+
let init;
|
|
189
|
+
try {
|
|
190
|
+
init = await headers(null);
|
|
191
|
+
} catch {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
init.set("accept", "text/event-stream");
|
|
195
|
+
const resumed = lastId !== null;
|
|
196
|
+
if (lastId !== null) init.set("last-event-id", lastId);
|
|
197
|
+
let response = null;
|
|
198
|
+
try {
|
|
199
|
+
response = await fetcher(options.endpoint, {
|
|
200
|
+
cache: "no-store",
|
|
201
|
+
credentials,
|
|
202
|
+
headers: init,
|
|
203
|
+
method: "GET",
|
|
204
|
+
signal
|
|
205
|
+
});
|
|
206
|
+
} catch {}
|
|
207
|
+
if (signal.aborted) return;
|
|
208
|
+
const type = response?.headers.get("content-type") ?? "";
|
|
209
|
+
if (response !== null && (FINAL_STREAM_STATUSES.has(response.status) || type.toLowerCase().startsWith("text/html"))) {
|
|
210
|
+
await response.body?.cancel().catch(() => void 0);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const version = response?.headers.get(VERSION_HEADER) ?? null;
|
|
214
|
+
const opened = Date.now();
|
|
215
|
+
let closed = false;
|
|
216
|
+
if (response !== null && response.ok && version !== null && response.body !== null && type.toLowerCase().startsWith("text/event-stream")) {
|
|
217
|
+
yield {
|
|
218
|
+
replayed: resumed,
|
|
219
|
+
type: "open"
|
|
220
|
+
};
|
|
221
|
+
yield {
|
|
222
|
+
type: "version",
|
|
223
|
+
version
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
for await (const frame of sseFrames(response.body, signal)) {
|
|
227
|
+
if (frame.retry !== null) retryMs = frame.retry;
|
|
228
|
+
if (frame.id !== null) lastId = frame.id === "" ? null : frame.id;
|
|
229
|
+
const event = frame.data === "" ? null : parsePushEvent(frame.data);
|
|
230
|
+
if (event !== null) {
|
|
231
|
+
closed ||= event.type === "close";
|
|
232
|
+
yield event;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
} else await response?.body?.cancel().catch(() => void 0);
|
|
237
|
+
if (closed || Date.now() - opened >= STABLE_MS) failures = 0;
|
|
238
|
+
else failures += 1;
|
|
239
|
+
await pause(Math.min(retryMs * 2 ** Math.max(0, failures - 1), MAX_RETRY_MS), signal);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
108
242
|
frame: options.endpoint,
|
|
243
|
+
async page(request, pageOptions) {
|
|
244
|
+
const response = await send(JSON.stringify(request.props === void 0 ? {
|
|
245
|
+
name: request.name,
|
|
246
|
+
type: "page"
|
|
247
|
+
} : {
|
|
248
|
+
name: request.name,
|
|
249
|
+
props: request.props,
|
|
250
|
+
type: "page"
|
|
251
|
+
}), pageOptions.signal);
|
|
252
|
+
const reply = parsePageReply(await replyBody(response));
|
|
253
|
+
if (reply === null) throw new TransportError(unavailable);
|
|
254
|
+
return reply;
|
|
255
|
+
},
|
|
109
256
|
async *session(body, { signal }) {
|
|
110
257
|
let init;
|
|
111
258
|
try {
|
|
@@ -120,6 +267,7 @@ function httpTransport(options) {
|
|
|
120
267
|
let response;
|
|
121
268
|
try {
|
|
122
269
|
response = await post(JSON.stringify({
|
|
270
|
+
document: body.document,
|
|
123
271
|
messages: body.messages,
|
|
124
272
|
options: body.options,
|
|
125
273
|
type: "session"
|
|
@@ -138,40 +286,24 @@ function httpTransport(options) {
|
|
|
138
286
|
};
|
|
139
287
|
return;
|
|
140
288
|
}
|
|
141
|
-
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
142
|
-
let buffer = "";
|
|
143
289
|
let ended = false;
|
|
144
290
|
try {
|
|
145
|
-
for (
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
type: "error"
|
|
151
|
-
};
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
buffer += chunk.value;
|
|
155
|
-
let end = buffer.indexOf("\n\n");
|
|
156
|
-
while (end !== -1) {
|
|
157
|
-
const frame = buffer.slice(0, end);
|
|
158
|
-
buffer = buffer.slice(end + 2);
|
|
159
|
-
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
160
|
-
const event = data === "" ? null : parseSessionEvent(data);
|
|
161
|
-
if (event !== null) {
|
|
162
|
-
ended ||= event.type !== "program";
|
|
163
|
-
yield event;
|
|
164
|
-
}
|
|
165
|
-
end = buffer.indexOf("\n\n");
|
|
291
|
+
for await (const frame of sseFrames(response.body, signal)) {
|
|
292
|
+
const event = frame.data === "" ? null : parseSessionEvent(frame.data);
|
|
293
|
+
if (event !== null) {
|
|
294
|
+
ended ||= event.type !== "program";
|
|
295
|
+
yield event;
|
|
166
296
|
}
|
|
167
297
|
}
|
|
298
|
+
if (!ended && !signal.aborted) yield {
|
|
299
|
+
error: network,
|
|
300
|
+
type: "error"
|
|
301
|
+
};
|
|
168
302
|
} catch {
|
|
169
303
|
if (!signal.aborted) yield {
|
|
170
304
|
error: network,
|
|
171
305
|
type: "error"
|
|
172
306
|
};
|
|
173
|
-
} finally {
|
|
174
|
-
reader.releaseLock();
|
|
175
307
|
}
|
|
176
308
|
}
|
|
177
309
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfirmFunction, JigsClient } from "../client.js";
|
|
2
|
-
import { Program, PropSchema, RuntimeError, WireArg } from "@jigs.design/protocol";
|
|
2
|
+
import { JsonValue, Program, PropSchema, RuntimeError, WireArg } from "@jigs.design/protocol";
|
|
3
3
|
import { TreeNode, TreeStore } from "@jigs.design/runtime";
|
|
4
4
|
//#region src/view/controller.d.ts
|
|
5
5
|
/** What the view shows beside the tree: a snapshot, replaced on each change. */
|
|
@@ -91,6 +91,12 @@ export interface ViewController {
|
|
|
91
91
|
* `signal` aborted.
|
|
92
92
|
*/
|
|
93
93
|
setConfirmHandler: (handler: ConfirmFunction | undefined) => void;
|
|
94
|
+
/**
|
|
95
|
+
* Replaces the provided data, by slot name. Every read of a slot whose
|
|
96
|
+
* value changed, compared as JSON, gets the new value, as a refetched
|
|
97
|
+
* query's does; a slot left out reads as `UNAVAILABLE`.
|
|
98
|
+
*/
|
|
99
|
+
setData: (values: Readonly<Record<string, JsonValue>> | undefined) => void;
|
|
94
100
|
/** Replaces what each new issue is given to; `undefined` stops reporting. */
|
|
95
101
|
setIssueHandler: (handler: ((issue: ViewIssue) => void) | undefined) => void;
|
|
96
102
|
/**
|
package/dist/view/controller.js
CHANGED
|
@@ -35,6 +35,15 @@ const NO_ACTIONS = {
|
|
|
35
35
|
},
|
|
36
36
|
status: "error"
|
|
37
37
|
};
|
|
38
|
+
/** What a read of slot `name` gets when the page provides no value for it. */
|
|
39
|
+
const notProvided = (name) => ({
|
|
40
|
+
data: null,
|
|
41
|
+
error: {
|
|
42
|
+
code: "UNAVAILABLE",
|
|
43
|
+
message: `The page provided no "${name}"`
|
|
44
|
+
},
|
|
45
|
+
status: "error"
|
|
46
|
+
});
|
|
38
47
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
48
|
/** The runtime errors of a result, as text React can draw. The frame is not trusted. */
|
|
40
49
|
function parseErrors(list) {
|
|
@@ -73,6 +82,10 @@ function createViewController(client) {
|
|
|
73
82
|
const store = createTreeStore();
|
|
74
83
|
const listeners = /* @__PURE__ */ new Set();
|
|
75
84
|
const observers = /* @__PURE__ */ new Map();
|
|
85
|
+
/** Reads of provided slots, answered here, never fetched: key to slot name. */
|
|
86
|
+
const slotReads = /* @__PURE__ */ new Map();
|
|
87
|
+
/** The page's provided data, each value with its JSON to compare by. */
|
|
88
|
+
let provided = /* @__PURE__ */ new Map();
|
|
76
89
|
const sent = /* @__PURE__ */ new Map();
|
|
77
90
|
let iframe = null;
|
|
78
91
|
let ready = false;
|
|
@@ -107,6 +120,8 @@ function createViewController(client) {
|
|
|
107
120
|
* still showing cannot run a mutation for a program that is gone.
|
|
108
121
|
*/
|
|
109
122
|
let confirmationScope = new AbortController();
|
|
123
|
+
/** Lets the push channel close; set while the view is started. */
|
|
124
|
+
let unhold = null;
|
|
110
125
|
const withdrawConfirmations = () => {
|
|
111
126
|
confirmationScope.abort();
|
|
112
127
|
confirmationScope = new AbortController();
|
|
@@ -241,8 +256,18 @@ function createViewController(client) {
|
|
|
241
256
|
const dropObservers = () => {
|
|
242
257
|
for (const unsubscribe of observers.values()) unsubscribe();
|
|
243
258
|
observers.clear();
|
|
259
|
+
slotReads.clear();
|
|
244
260
|
sent.clear();
|
|
245
261
|
};
|
|
262
|
+
/** What a read of slot `name` gets now. */
|
|
263
|
+
const slotResult = (name) => {
|
|
264
|
+
const entry = provided.get(name);
|
|
265
|
+
return entry === void 0 ? notProvided(name) : {
|
|
266
|
+
data: entry.value,
|
|
267
|
+
error: null,
|
|
268
|
+
status: "success"
|
|
269
|
+
};
|
|
270
|
+
};
|
|
246
271
|
/** The running program's entry for `name`, if it is one of `kinds`. */
|
|
247
272
|
const procedure = (name, kinds) => {
|
|
248
273
|
if (running === null || !Object.hasOwn(running.procedures, name)) return;
|
|
@@ -253,7 +278,7 @@ function createViewController(client) {
|
|
|
253
278
|
const checkedReads = (reads) => {
|
|
254
279
|
const wanted = /* @__PURE__ */ new Map();
|
|
255
280
|
for (const read of reads) {
|
|
256
|
-
if (!isRecord(read) || typeof read.key !== "string" || typeof read.name !== "string" || procedure(read.name, ["query"]) === void 0) {
|
|
281
|
+
if (!isRecord(read) || typeof read.key !== "string" || typeof read.name !== "string" || procedure(read.name, ["data", "query"]) === void 0) {
|
|
257
282
|
drop("The sandbox read a query this program does not declare");
|
|
258
283
|
continue;
|
|
259
284
|
}
|
|
@@ -275,9 +300,20 @@ function createViewController(client) {
|
|
|
275
300
|
observers.delete(key);
|
|
276
301
|
sent.delete(key);
|
|
277
302
|
}
|
|
278
|
-
for (const
|
|
279
|
-
|
|
280
|
-
|
|
303
|
+
for (const key of slotReads.keys()) if (!wanted.has(key)) {
|
|
304
|
+
slotReads.delete(key);
|
|
305
|
+
sent.delete(key);
|
|
306
|
+
}
|
|
307
|
+
for (const [key, read] of wanted) {
|
|
308
|
+
if (procedure(read.name, ["data"]) !== void 0) {
|
|
309
|
+
slotReads.set(key, read.name);
|
|
310
|
+
sendQuery(key, slotResult(read.name));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!observers.has(key)) observers.set(key, client.observe(read.name, read.input, (result) => {
|
|
314
|
+
sendQuery(key, result);
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
281
317
|
};
|
|
282
318
|
/** Marks the call `answered` done; returns whether it was a handler call. */
|
|
283
319
|
const answer = (answered) => {
|
|
@@ -478,6 +514,19 @@ function createViewController(client) {
|
|
|
478
514
|
setConfirmHandler: (handler) => {
|
|
479
515
|
confirmHandler = handler;
|
|
480
516
|
},
|
|
517
|
+
setData: (values) => {
|
|
518
|
+
const next = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
520
|
+
const text = JSON.stringify(value);
|
|
521
|
+
const before = provided.get(name);
|
|
522
|
+
next.set(name, before?.text === text ? before : {
|
|
523
|
+
text,
|
|
524
|
+
value
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
provided = next;
|
|
528
|
+
for (const [key, name] of slotReads) sendQuery(key, slotResult(name));
|
|
529
|
+
},
|
|
481
530
|
setIssueHandler: (handler) => {
|
|
482
531
|
issueHandler = handler;
|
|
483
532
|
},
|
|
@@ -497,6 +546,7 @@ function createViewController(client) {
|
|
|
497
546
|
if (iframe === frame && !ready && startup === null) startup = setTimeout(diagnoseFrame, STARTUP_MS);
|
|
498
547
|
});
|
|
499
548
|
iframe = frame;
|
|
549
|
+
unhold = client.hold();
|
|
500
550
|
window.addEventListener("message", onMessage);
|
|
501
551
|
document.body.append(frame);
|
|
502
552
|
},
|
|
@@ -520,8 +570,11 @@ function createViewController(client) {
|
|
|
520
570
|
latestDocument = void 0;
|
|
521
571
|
actionHandler = void 0;
|
|
522
572
|
confirmHandler = void 0;
|
|
573
|
+
provided = /* @__PURE__ */ new Map();
|
|
523
574
|
withdrawConfirmations();
|
|
524
575
|
dropObservers();
|
|
576
|
+
unhold?.();
|
|
577
|
+
unhold = null;
|
|
525
578
|
store.reset();
|
|
526
579
|
status = idle;
|
|
527
580
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigs.design/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "createJigsClient: the browser's transport and query cache. No React.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@tanstack/query-core": "5.104.4",
|
|
29
|
-
"@jigs.design/
|
|
30
|
-
"@jigs.design/
|
|
29
|
+
"@jigs.design/runtime": "0.3.1",
|
|
30
|
+
"@jigs.design/protocol": "0.3.1"
|
|
31
31
|
}
|
|
32
32
|
}
|