@panphora/clayjs 1.2.0 → 1.4.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/README.md +7 -3
- package/THIRD-PARTY-NOTICES.md +10 -0
- package/dist/clay.standalone.js +22539 -14260
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +1 -1
- package/package.json +7 -2
- package/packed-contract.json +17 -0
- package/src/attrs/save-freeze.js +10 -18
- package/src/core/admin-contenteditable.js +8 -6
- package/src/core/admin-inputs.js +23 -13
- package/src/core/admin-onclick.js +5 -0
- package/src/core/is-edit-mode.js +7 -2
- package/src/core/persist.js +5 -10
- package/src/core/save-core.js +35 -0
- package/src/core/save.js +13 -1
- package/src/core/snapshot.js +186 -14
- package/src/core/source-map.js +1025 -0
- package/src/core/unsaved-warning.js +3 -0
- package/src/dom/dom-helpers.js +5 -1
- package/src/lib/content-dom.js +108 -0
- package/src/lib/mutation.js +26 -3
- package/src/lib/region-capabilities.js +69 -0
- package/src/lib/region-policy.js +18 -13
- package/src/loader-logic.js +27 -5
- package/src/loader.js +6 -0
- package/src/plugins/ai-edit.js +625 -0
- package/src/plugins/demo.js +3 -0
- package/src/plugins/sortable.js +6 -1
- package/src/plugins/source.js +410 -0
- package/src/plugins/wire.js +248 -47
- package/src/sync/live-sync.js +106 -42
- package/src/sync/presence.js +303 -0
- package/src/sync/section-notice.js +230 -0
- package/src/sync/splice-merge.js +7 -10
- package/src/sync/stream.js +190 -0
- package/src/vendor/control-serialize.vendor.js +10 -6
- package/src/vendor/hyper-morph.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +438 -45
- package/src/vendor/parse5.vendor.js +3 -0
- package/src/vendor/quickcrop.vendor.js +1 -1
- package/src/vendor/richclay.vendor.js +22 -15
package/src/plugins/wire.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { isEditMode } from "../core/is-edit-mode.js";
|
|
2
|
+
import { saveToken } from "../core/host-attrs.js";
|
|
3
|
+
import { hostMeta, hostSupports } from "../core/host-meta.js";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* clay.wire — a per-file control channel between this page and a local process.
|
|
5
7
|
*
|
|
6
8
|
* The page sends a request ("rewrite the section I circled"), a process running
|
|
7
|
-
* in the user's own terminal answers with progress and a terminal frame
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* about what a payload contains.
|
|
9
|
+
* in the user's own terminal answers with progress and a terminal frame. An
|
|
10
|
+
* editing process changes the file and the result reaches this page through
|
|
11
|
+
* live-sync. A structured helper can instead return a bounded result on the
|
|
12
|
+
* terminal frame without touching the document.
|
|
12
13
|
*
|
|
13
14
|
* Three constraints shape everything below.
|
|
14
15
|
*
|
|
@@ -44,13 +45,14 @@ import { isEditMode } from "../core/is-edit-mode.js";
|
|
|
44
45
|
// only has to say so.
|
|
45
46
|
const ACK_TIMEOUT_MS = 15000;
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
+
// Raw handlers keep an inactivity deadline: every frame rearms it, and `wire
|
|
48
49
|
// serve` streams the child's stdout as status frames, so an agent that reports
|
|
49
|
-
// what it is doing keeps its request alive indefinitely. A
|
|
50
|
-
//
|
|
51
|
-
// since the wire never carried the content anyway.
|
|
50
|
+
// what it is doing keeps its request alive indefinitely. A structured handler
|
|
51
|
+
// instead announces its fixed execution budget in the acknowledgement.
|
|
52
52
|
const SILENCE_TIMEOUT_MS = 120000;
|
|
53
53
|
|
|
54
|
+
const STRUCTURED_TIMEOUT_GRACE_MS = 15000;
|
|
55
|
+
|
|
54
56
|
// `wire/done` means the handler finished writing the file. It does not mean this
|
|
55
57
|
// page has rendered the change: that arrives on the live-sync lane after the
|
|
56
58
|
// watcher's quiet interval, and nothing orders the two. So a finished request
|
|
@@ -72,6 +74,8 @@ const OPEN_STATES = new Set(["sent", "acked"]);
|
|
|
72
74
|
// for a request the handler also errored.
|
|
73
75
|
const TERMINAL_STATES = new Set(["done", "error", "cancelled"]);
|
|
74
76
|
|
|
77
|
+
const HELPER_EXTENSION = "wire";
|
|
78
|
+
|
|
75
79
|
const records = new Map();
|
|
76
80
|
const listeners = new Set();
|
|
77
81
|
|
|
@@ -94,13 +98,34 @@ function wireURL(path) {
|
|
|
94
98
|
return new URL(path, window.location.origin).href;
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
// The document's save token rides on both legs of the wire.
|
|
102
|
+
//
|
|
103
|
+
// A helper-bound channel is the one place "one wire per file" has to be
|
|
104
|
+
// isolation rather than addressing. A page names only its own URL, but the host
|
|
105
|
+
// resolves that through the same funnel the save route uses, and that funnel
|
|
106
|
+
// admits any registered path on this origin. Without the token, another
|
|
107
|
+
// document's page on the same loopback origin could subscribe to this file's
|
|
108
|
+
// channel and push requests that run this file's helpers.
|
|
109
|
+
//
|
|
110
|
+
// A host that has no helper bound to the file ignores the token on both legs, so
|
|
111
|
+
// this is unconditional here and costs a document nothing.
|
|
112
|
+
function wireToken() {
|
|
113
|
+
const token = saveToken();
|
|
114
|
+
return typeof token === "string" && token !== "" ? token : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
function view(rec) {
|
|
98
118
|
return {
|
|
99
119
|
id: rec.id,
|
|
100
120
|
type: rec.type,
|
|
101
121
|
state: rec.state,
|
|
122
|
+
helper: rec.helper,
|
|
102
123
|
text: rec.text,
|
|
124
|
+
result: rec.result,
|
|
103
125
|
error: rec.error,
|
|
126
|
+
errorCode: rec.errorCode,
|
|
127
|
+
errorDetails: rec.errorDetails,
|
|
128
|
+
errorSource: rec.errorSource,
|
|
104
129
|
startedAt: rec.startedAt,
|
|
105
130
|
};
|
|
106
131
|
}
|
|
@@ -129,13 +154,14 @@ function setState(rec, state) {
|
|
|
129
154
|
emit(rec);
|
|
130
155
|
}
|
|
131
156
|
|
|
132
|
-
// The request deadline. Armed before the POST
|
|
133
|
-
//
|
|
134
|
-
function arm(rec, ms, message) {
|
|
157
|
+
// The request deadline. Armed before the POST, then replaced by the handler's
|
|
158
|
+
// raw inactivity window or structured execution budget when it acknowledges.
|
|
159
|
+
function arm(rec, ms, message, { cancelFirst = false, errorCode = null } = {}) {
|
|
135
160
|
clearTimeout(rec.timer);
|
|
136
161
|
rec.timer = setTimeout(() => {
|
|
137
162
|
rec.timer = null;
|
|
138
|
-
|
|
163
|
+
if (cancelFirst) postCancel(rec);
|
|
164
|
+
finish(rec, "error", message, { errorCode, errorSource: "client" });
|
|
139
165
|
}, ms);
|
|
140
166
|
}
|
|
141
167
|
|
|
@@ -162,7 +188,11 @@ function prune() {
|
|
|
162
188
|
function openStream() {
|
|
163
189
|
if (streamReady) return streamReady;
|
|
164
190
|
|
|
165
|
-
|
|
191
|
+
// In the query, not a header: native EventSource takes no custom headers, and
|
|
192
|
+
// the access log records URL.Path without the query.
|
|
193
|
+
const token = wireToken();
|
|
194
|
+
const path = `/_/wire/subscribe?page-url=${encodeURIComponent(window.location.href)}`
|
|
195
|
+
+ (token ? `&token=${encodeURIComponent(token)}` : "");
|
|
166
196
|
stream = new EventSource(wireURL(path));
|
|
167
197
|
|
|
168
198
|
stream.onmessage = (event) => {
|
|
@@ -222,26 +252,88 @@ function handleFrame(frame) {
|
|
|
222
252
|
// Rearmed, not cleared. The handler acknowledges within milliseconds of
|
|
223
253
|
// picking a request up, so clearing here would leave every working request
|
|
224
254
|
// with no deadline at all.
|
|
225
|
-
|
|
255
|
+
if (
|
|
256
|
+
frame.payload?.mode === "jsonl" &&
|
|
257
|
+
Number.isFinite(frame.payload.budgetMs) &&
|
|
258
|
+
frame.payload.budgetMs > 0
|
|
259
|
+
) {
|
|
260
|
+
rec.structured = true;
|
|
261
|
+
arm(
|
|
262
|
+
rec,
|
|
263
|
+
frame.payload.budgetMs + STRUCTURED_TIMEOUT_GRACE_MS,
|
|
264
|
+
"the helper did not finish within its execution budget",
|
|
265
|
+
{ cancelFirst: true, errorCode: "helper_timeout" }
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
arm(rec, SILENCE_TIMEOUT_MS, "the agent stopped responding");
|
|
269
|
+
}
|
|
226
270
|
setState(rec, "acked");
|
|
227
271
|
break;
|
|
228
272
|
case "wire/status":
|
|
229
|
-
arm(rec, SILENCE_TIMEOUT_MS, "the agent stopped responding");
|
|
273
|
+
if (!rec.structured) arm(rec, SILENCE_TIMEOUT_MS, "the agent stopped responding");
|
|
230
274
|
rec.text = typeof frame.text === "string" ? frame.text : "";
|
|
231
275
|
rec.state = "acked";
|
|
276
|
+
notifyStatus(rec, frame);
|
|
232
277
|
emit(rec, frame);
|
|
233
278
|
break;
|
|
234
279
|
case "wire/done":
|
|
235
|
-
|
|
280
|
+
rec.result = Object.prototype.hasOwnProperty.call(frame, "payload") ? frame.payload : null;
|
|
281
|
+
if (rec.document === "edit") land(rec);
|
|
282
|
+
else finish(rec, "done", null);
|
|
236
283
|
break;
|
|
237
284
|
case "wire/error":
|
|
238
|
-
|
|
285
|
+
finishError(rec, frame);
|
|
239
286
|
break;
|
|
240
287
|
default:
|
|
241
288
|
break;
|
|
242
289
|
}
|
|
243
290
|
}
|
|
244
291
|
|
|
292
|
+
function notifyStatus(rec, frame) {
|
|
293
|
+
if (!rec.onStatus || typeof frame.text !== "string") return;
|
|
294
|
+
const status = { text: frame.text };
|
|
295
|
+
if (
|
|
296
|
+
frame.payload &&
|
|
297
|
+
typeof frame.payload === "object" &&
|
|
298
|
+
Object.prototype.hasOwnProperty.call(frame.payload, "progress")
|
|
299
|
+
) {
|
|
300
|
+
status.progress = frame.payload.progress;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
rec.onStatus(status);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error("clay.wire: an onStatus listener threw", err);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function finishError(rec, frame) {
|
|
310
|
+
const payload = frame.payload;
|
|
311
|
+
const structured =
|
|
312
|
+
payload &&
|
|
313
|
+
typeof payload === "object" &&
|
|
314
|
+
!Array.isArray(payload) &&
|
|
315
|
+
(payload.source === "application" || payload.source === "host") &&
|
|
316
|
+
typeof payload.code === "string" &&
|
|
317
|
+
payload.code;
|
|
318
|
+
const error = typeof frame.text === "string" && frame.text
|
|
319
|
+
? frame.text
|
|
320
|
+
: "the handler reported an error";
|
|
321
|
+
|
|
322
|
+
if (!structured) {
|
|
323
|
+
finish(rec, "error", error);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const state = payload.source === "host" && payload.code === "helper_cancelled"
|
|
328
|
+
? "cancelled"
|
|
329
|
+
: "error";
|
|
330
|
+
finish(rec, state, error, {
|
|
331
|
+
errorCode: payload.code,
|
|
332
|
+
errorDetails: Object.prototype.hasOwnProperty.call(payload, "details") ? payload.details : null,
|
|
333
|
+
errorSource: payload.source,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
245
337
|
// --- the landing state -----------------------------------------------------
|
|
246
338
|
|
|
247
339
|
// `clay:sync-applied` fires on every successful remote morph, from live-sync's
|
|
@@ -292,7 +384,7 @@ function land(rec) {
|
|
|
292
384
|
closeStreamIfIdle();
|
|
293
385
|
}
|
|
294
386
|
|
|
295
|
-
function finish(rec, state, error) {
|
|
387
|
+
function finish(rec, state, error, fields = {}) {
|
|
296
388
|
if (TERMINAL_STATES.has(rec.state)) return;
|
|
297
389
|
const wasLanding = rec.state === "landing";
|
|
298
390
|
disarm(rec);
|
|
@@ -306,7 +398,14 @@ function finish(rec, state, error) {
|
|
|
306
398
|
unwatchLandingsIfIdle();
|
|
307
399
|
}
|
|
308
400
|
rec.error = error;
|
|
401
|
+
if (Object.prototype.hasOwnProperty.call(fields, "errorCode")) rec.errorCode = fields.errorCode;
|
|
402
|
+
if (Object.prototype.hasOwnProperty.call(fields, "errorDetails")) rec.errorDetails = fields.errorDetails;
|
|
403
|
+
if (Object.prototype.hasOwnProperty.call(fields, "errorSource")) rec.errorSource = fields.errorSource;
|
|
309
404
|
rec.state = state;
|
|
405
|
+
if (rec.signal && rec.signalHandler) {
|
|
406
|
+
rec.signal.removeEventListener("abort", rec.signalHandler);
|
|
407
|
+
rec.signalHandler = null;
|
|
408
|
+
}
|
|
310
409
|
releaseSaving(rec);
|
|
311
410
|
emit(rec);
|
|
312
411
|
closeStreamIfIdle();
|
|
@@ -432,12 +531,15 @@ async function flushSave() {
|
|
|
432
531
|
// --- sending ---------------------------------------------------------------
|
|
433
532
|
|
|
434
533
|
async function postFrame(body, signal) {
|
|
534
|
+
const headers = {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
"Page-URL": window.location.href,
|
|
537
|
+
};
|
|
538
|
+
const token = wireToken();
|
|
539
|
+
if (token) headers["Save-Token"] = token;
|
|
435
540
|
const response = await fetch(wireURL("/_/wire/send"), {
|
|
436
541
|
method: "POST",
|
|
437
|
-
headers
|
|
438
|
-
"Content-Type": "application/json",
|
|
439
|
-
"Page-URL": window.location.href,
|
|
440
|
-
},
|
|
542
|
+
headers,
|
|
441
543
|
body: JSON.stringify(body),
|
|
442
544
|
signal,
|
|
443
545
|
});
|
|
@@ -460,42 +562,59 @@ async function postFrame(body, signal) {
|
|
|
460
562
|
function send(payload, opts = {}) {
|
|
461
563
|
const id = typeof opts.id === "string" && opts.id ? opts.id : newId();
|
|
462
564
|
const type = typeof opts.type === "string" && opts.type ? opts.type : "wire/request";
|
|
565
|
+
const hasHelper = Object.prototype.hasOwnProperty.call(opts, "helper");
|
|
566
|
+
const helper = hasHelper && typeof opts.helper === "string" && opts.helper ? opts.helper : null;
|
|
567
|
+
const documentMode = opts.document === undefined ? (helper ? "none" : "edit") : opts.document;
|
|
568
|
+
|
|
569
|
+
if (hasHelper && !helper) {
|
|
570
|
+
return rejected(id, type, null, "helper must be a nonempty string", "invalid_helper");
|
|
571
|
+
}
|
|
572
|
+
if (documentMode !== "edit" && documentMode !== "none") {
|
|
573
|
+
return rejected(
|
|
574
|
+
id,
|
|
575
|
+
type,
|
|
576
|
+
helper,
|
|
577
|
+
'document must be "edit" or "none"',
|
|
578
|
+
"invalid_document"
|
|
579
|
+
);
|
|
580
|
+
}
|
|
463
581
|
|
|
464
582
|
// The id is request identity on the wire, on both sides: a reused one would
|
|
465
583
|
// route the first request's frames to the second record, leaving the first
|
|
466
584
|
// unresolvable and its save hold never released. Refused as an outcome rather
|
|
467
585
|
// than thrown, so a UI renders it the way it renders any other failure.
|
|
468
586
|
if (records.has(id)) {
|
|
469
|
-
|
|
587
|
+
return rejected(
|
|
470
588
|
id,
|
|
471
589
|
type,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
};
|
|
477
|
-
return {
|
|
478
|
-
id,
|
|
479
|
-
get state() {
|
|
480
|
-
return clash.state;
|
|
481
|
-
},
|
|
482
|
-
done: Promise.resolve(view(clash)),
|
|
483
|
-
cancel: () => false,
|
|
484
|
-
};
|
|
590
|
+
helper,
|
|
591
|
+
"a request with this id is already on the wire",
|
|
592
|
+
"duplicate_request"
|
|
593
|
+
);
|
|
485
594
|
}
|
|
486
595
|
|
|
487
596
|
const rec = {
|
|
488
597
|
id,
|
|
489
598
|
type,
|
|
490
599
|
state: "sent",
|
|
600
|
+
helper,
|
|
601
|
+
document: documentMode,
|
|
602
|
+
structured: false,
|
|
603
|
+
onStatus: typeof opts.onStatus === "function" ? opts.onStatus : null,
|
|
491
604
|
text: "",
|
|
605
|
+
result: null,
|
|
492
606
|
error: null,
|
|
607
|
+
errorCode: null,
|
|
608
|
+
errorDetails: null,
|
|
609
|
+
errorSource: null,
|
|
493
610
|
startedAt: Date.now(),
|
|
494
611
|
timer: null,
|
|
495
612
|
landingTimer: null,
|
|
496
613
|
abort: typeof AbortController === "function" ? new AbortController() : null,
|
|
497
614
|
holdsSave: false,
|
|
498
615
|
settle: null,
|
|
616
|
+
signal: opts.signal && typeof opts.signal.addEventListener === "function" ? opts.signal : null,
|
|
617
|
+
signalHandler: null,
|
|
499
618
|
};
|
|
500
619
|
rec.done = new Promise((resolve) => {
|
|
501
620
|
rec.settle = resolve;
|
|
@@ -503,15 +622,36 @@ function send(payload, opts = {}) {
|
|
|
503
622
|
records.set(id, rec);
|
|
504
623
|
emit(rec);
|
|
505
624
|
|
|
625
|
+
if (rec.signal) {
|
|
626
|
+
rec.signalHandler = () => cancel(id);
|
|
627
|
+
rec.signal.addEventListener("abort", rec.signalHandler, { once: true });
|
|
628
|
+
if (rec.signal.aborted) rec.signalHandler();
|
|
629
|
+
}
|
|
630
|
+
|
|
506
631
|
const dispatch = async () => {
|
|
507
632
|
// The record is re-checked after every await. A cancel can land in any of
|
|
508
633
|
// these gaps, and a step that ran on regardless would open a stream nobody
|
|
509
634
|
// closes or post a request the user already took back.
|
|
510
|
-
await holdSaving(rec);
|
|
511
635
|
if (rec.state !== "sent") return;
|
|
636
|
+
if (rec.helper) {
|
|
637
|
+
const supported = await hostSupports(HELPER_EXTENSION);
|
|
638
|
+
if (rec.state !== "sent") return;
|
|
639
|
+
if (!supported) {
|
|
640
|
+
finish(rec, "error", "this host does not support named wire helpers", {
|
|
641
|
+
errorCode: "helper_protocol_unsupported",
|
|
642
|
+
errorSource: "client",
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
512
647
|
|
|
513
|
-
|
|
514
|
-
|
|
648
|
+
if (rec.document === "edit") {
|
|
649
|
+
await holdSaving(rec);
|
|
650
|
+
if (rec.state !== "sent") return;
|
|
651
|
+
|
|
652
|
+
await flushSave();
|
|
653
|
+
if (rec.state !== "sent") return;
|
|
654
|
+
}
|
|
515
655
|
|
|
516
656
|
// Subscribe before posting. A handler can acknowledge in single-digit
|
|
517
657
|
// milliseconds, a fresh subscription deliberately replays nothing, and only
|
|
@@ -527,13 +667,18 @@ function send(payload, opts = {}) {
|
|
|
527
667
|
// the request unbounded, and an ack that arrives while the POST is still in
|
|
528
668
|
// flight — the ordinary case, since the page subscribed first — would be
|
|
529
669
|
// followed by a fresh ack timer that fails a healthy request 15s later.
|
|
530
|
-
arm(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
rec.
|
|
670
|
+
arm(
|
|
671
|
+
rec,
|
|
672
|
+
ACK_TIMEOUT_MS,
|
|
673
|
+
"the agent never answered",
|
|
674
|
+
rec.helper ? { cancelFirst: true, errorCode: "ack_timeout" } : undefined
|
|
535
675
|
);
|
|
536
676
|
|
|
677
|
+
const body = { type: rec.type, id: rec.id, text: opts.text, payload };
|
|
678
|
+
if (rec.helper) body.helper = rec.helper;
|
|
679
|
+
if (rec.helper || opts.document !== undefined) body.document = rec.document;
|
|
680
|
+
const { ok, status, reply } = await postFrame(body, rec.abort?.signal);
|
|
681
|
+
|
|
537
682
|
// A frame may have moved this request on while its own POST was in flight.
|
|
538
683
|
// Only a request still waiting on that POST may be failed by it.
|
|
539
684
|
if (rec.state !== "sent") return;
|
|
@@ -542,6 +687,20 @@ function send(payload, opts = {}) {
|
|
|
542
687
|
finish(rec, "error", `the wire refused this request (${status})`);
|
|
543
688
|
return;
|
|
544
689
|
}
|
|
690
|
+
// A typed refusal on the reply beats the generic reading of delivered: 0.
|
|
691
|
+
// The host sends the same refusal down the stream, but the reply and the
|
|
692
|
+
// stream are two connections with no ordering between them, so settling on
|
|
693
|
+
// delivered: 0 first would discard it and report "no agent is attached" for a
|
|
694
|
+
// handler that was attached and refused for a specific, reportable reason.
|
|
695
|
+
const refused = reply && reply.refused;
|
|
696
|
+
if (refused && typeof refused.code === "string" && refused.code) {
|
|
697
|
+
finish(rec, "error", refused.message || "the handler refused this request", {
|
|
698
|
+
errorCode: refused.code,
|
|
699
|
+
errorSource: refused.source === "application" ? "application" : "host",
|
|
700
|
+
errorDetails: null,
|
|
701
|
+
});
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
545
704
|
if (!reply || reply.delivered === 0) {
|
|
546
705
|
// Accepted by the router and taken by nobody. Reporting this as an error
|
|
547
706
|
// rather than a pending request is the difference between a UI that says
|
|
@@ -564,6 +723,30 @@ function send(payload, opts = {}) {
|
|
|
564
723
|
};
|
|
565
724
|
}
|
|
566
725
|
|
|
726
|
+
function rejected(id, type, helper, error, errorCode) {
|
|
727
|
+
const rec = {
|
|
728
|
+
id,
|
|
729
|
+
type,
|
|
730
|
+
state: "error",
|
|
731
|
+
helper,
|
|
732
|
+
text: "",
|
|
733
|
+
result: null,
|
|
734
|
+
error,
|
|
735
|
+
errorCode,
|
|
736
|
+
errorDetails: null,
|
|
737
|
+
errorSource: "client",
|
|
738
|
+
startedAt: Date.now(),
|
|
739
|
+
};
|
|
740
|
+
return {
|
|
741
|
+
id,
|
|
742
|
+
get state() {
|
|
743
|
+
return rec.state;
|
|
744
|
+
},
|
|
745
|
+
done: Promise.resolve(view(rec)),
|
|
746
|
+
cancel: () => false,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
567
750
|
/**
|
|
568
751
|
* Stop completely: the request ends here, its late frames are ignored, and the
|
|
569
752
|
* handler is asked to stop. A cancel that only hid the spinner would leave the
|
|
@@ -572,11 +755,15 @@ function send(payload, opts = {}) {
|
|
|
572
755
|
function cancel(id) {
|
|
573
756
|
const rec = records.get(id);
|
|
574
757
|
if (!rec || !OPEN_STATES.has(rec.state)) return false;
|
|
575
|
-
|
|
758
|
+
postCancel(rec);
|
|
576
759
|
finish(rec, "cancelled", null);
|
|
577
760
|
return true;
|
|
578
761
|
}
|
|
579
762
|
|
|
763
|
+
function postCancel(rec) {
|
|
764
|
+
postFrame({ type: "wire/cancel", id: rec.id }).catch(() => {});
|
|
765
|
+
}
|
|
766
|
+
|
|
580
767
|
function get(id) {
|
|
581
768
|
const rec = records.get(id);
|
|
582
769
|
return rec ? view(rec) : undefined;
|
|
@@ -599,6 +786,20 @@ function on(fn) {
|
|
|
599
786
|
return () => listeners.delete(fn);
|
|
600
787
|
}
|
|
601
788
|
|
|
602
|
-
|
|
789
|
+
async function helpers() {
|
|
790
|
+
const meta = await hostMeta({ fresh: true });
|
|
791
|
+
if (!meta.extensions.includes(HELPER_EXTENSION)) return [];
|
|
792
|
+
const declared = meta.document?.helpers;
|
|
793
|
+
if (!Array.isArray(declared)) return [];
|
|
794
|
+
return declared
|
|
795
|
+
.filter((helper) =>
|
|
796
|
+
helper &&
|
|
797
|
+
typeof helper.name === "string" &&
|
|
798
|
+
["ready", "denied", "unavailable"].includes(helper.state)
|
|
799
|
+
)
|
|
800
|
+
.map(({ name, state }) => ({ name, state }));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export const wire = { send, cancel, get, list, isBusy, on, helpers };
|
|
603
804
|
|
|
604
805
|
export default wire;
|