@jigs.design/client 0.3.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/dist/client.d.ts +7 -0
- package/dist/client.js +32 -12
- package/dist/view/controller.d.ts +7 -1
- package/dist/view/controller.js +57 -4
- package/package.json +3 -3
package/dist/client.d.ts
CHANGED
|
@@ -71,6 +71,13 @@ export interface PageResult {
|
|
|
71
71
|
export interface JigsClient {
|
|
72
72
|
/** URL of the sandbox frame document. */
|
|
73
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;
|
|
74
81
|
/** Refetches every query with this name, for every input. */
|
|
75
82
|
invalidate: (name: string) => Promise<void>;
|
|
76
83
|
/**
|
package/dist/client.js
CHANGED
|
@@ -6,9 +6,9 @@ const DEFAULT_STALE_TIME = 6e4;
|
|
|
6
6
|
const RETRYABLE = /* @__PURE__ */ new Set(["NETWORK", "UNAVAILABLE"]);
|
|
7
7
|
const MAX_RETRIES = 2;
|
|
8
8
|
/**
|
|
9
|
-
* How long the push channel stays open
|
|
10
|
-
* a
|
|
11
|
-
*
|
|
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
12
|
*/
|
|
13
13
|
const CHANNEL_LINGER_MS = 1e3;
|
|
14
14
|
/** When a channel the server refused or ended is tried again, with queries still observed. */
|
|
@@ -16,13 +16,16 @@ const CHANNEL_REOPEN_MS = 3e5;
|
|
|
16
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
17
|
const GAP_SPREAD_MS = 2e3;
|
|
18
18
|
/**
|
|
19
|
-
* The push channel:
|
|
20
|
-
*
|
|
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
|
|
21
22
|
* it cannot account for: a connection lost without a `close` event, to a
|
|
22
23
|
* server that did not replay what it missed.
|
|
23
24
|
*/
|
|
24
25
|
function createChannel(transport, handlers) {
|
|
25
26
|
let observers = 0;
|
|
27
|
+
/** Views mounted: they keep an open channel open, but do not open one. */
|
|
28
|
+
let holds = 0;
|
|
26
29
|
let running = null;
|
|
27
30
|
let linger;
|
|
28
31
|
let reopen;
|
|
@@ -53,16 +56,32 @@ function createChannel(transport, handlers) {
|
|
|
53
56
|
running = controller;
|
|
54
57
|
consume(transport.events({ signal: controller.signal }), controller.signal);
|
|
55
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
|
+
};
|
|
56
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
|
+
},
|
|
57
82
|
release: () => {
|
|
58
83
|
observers -= 1;
|
|
59
|
-
|
|
60
|
-
clearTimeout(linger);
|
|
61
|
-
linger = setTimeout(() => {
|
|
62
|
-
clearTimeout(reopen);
|
|
63
|
-
running?.abort();
|
|
64
|
-
running = null;
|
|
65
|
-
}, CHANNEL_LINGER_MS);
|
|
84
|
+
closeWhenIdle();
|
|
66
85
|
},
|
|
67
86
|
retain: () => {
|
|
68
87
|
observers += 1;
|
|
@@ -169,6 +188,7 @@ function createJigsClient(options) {
|
|
|
169
188
|
});
|
|
170
189
|
return {
|
|
171
190
|
frame: transport.frame,
|
|
191
|
+
hold: channel.hold,
|
|
172
192
|
invalidate: (name) => queries.invalidateQueries({ queryKey: [name] }),
|
|
173
193
|
invalidatePage,
|
|
174
194
|
mutate: async (name, input, procedure) => {
|
|
@@ -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.
|
|
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
|
}
|