@sparkletree/core 0.1.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/analytics.d.ts +75 -0
  4. package/dist/analytics.d.ts.map +1 -0
  5. package/dist/analytics.js +170 -0
  6. package/dist/analytics.js.map +1 -0
  7. package/dist/audio.d.ts +85 -0
  8. package/dist/audio.d.ts.map +1 -0
  9. package/dist/audio.js +465 -0
  10. package/dist/audio.js.map +1 -0
  11. package/dist/client.d.ts +79 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +251 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/context.d.ts +104 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +277 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/fragments.d.ts +108 -0
  20. package/dist/fragments.d.ts.map +1 -0
  21. package/dist/fragments.js +223 -0
  22. package/dist/fragments.js.map +1 -0
  23. package/dist/index.d.ts +31 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +30 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/protocol.d.ts +70 -0
  28. package/dist/protocol.d.ts.map +1 -0
  29. package/dist/protocol.js +97 -0
  30. package/dist/protocol.js.map +1 -0
  31. package/dist/publishableKey.d.ts +38 -0
  32. package/dist/publishableKey.d.ts.map +1 -0
  33. package/dist/publishableKey.js +69 -0
  34. package/dist/publishableKey.js.map +1 -0
  35. package/dist/safeUrl.d.ts +56 -0
  36. package/dist/safeUrl.d.ts.map +1 -0
  37. package/dist/safeUrl.js +117 -0
  38. package/dist/safeUrl.js.map +1 -0
  39. package/dist/sse.d.ts +51 -0
  40. package/dist/sse.d.ts.map +1 -0
  41. package/dist/sse.js +137 -0
  42. package/dist/sse.js.map +1 -0
  43. package/dist/state.d.ts +321 -0
  44. package/dist/state.d.ts.map +1 -0
  45. package/dist/state.js +594 -0
  46. package/dist/state.js.map +1 -0
  47. package/dist/text.d.ts +10 -0
  48. package/dist/text.d.ts.map +1 -0
  49. package/dist/text.js +29 -0
  50. package/dist/text.js.map +1 -0
  51. package/dist/theme.d.ts +52 -0
  52. package/dist/theme.d.ts.map +1 -0
  53. package/dist/theme.js +88 -0
  54. package/dist/theme.js.map +1 -0
  55. package/dist/trust.d.ts +52 -0
  56. package/dist/trust.d.ts.map +1 -0
  57. package/dist/trust.js +95 -0
  58. package/dist/trust.js.map +1 -0
  59. package/dist/variant.d.ts +117 -0
  60. package/dist/variant.d.ts.map +1 -0
  61. package/dist/variant.js +167 -0
  62. package/dist/variant.js.map +1 -0
  63. package/dist/wire.d.ts +564 -0
  64. package/dist/wire.d.ts.map +1 -0
  65. package/dist/wire.js +133 -0
  66. package/dist/wire.js.map +1 -0
  67. package/package.json +55 -0
package/dist/state.js ADDED
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Copy choreography — the rewrite/settle state machine.
3
+ *
4
+ * This is the part of the SDK that is not a fetch wrapper. The doctrine it
5
+ * implements (Squid V2, shipped 2026-07-23) is three rules:
6
+ *
7
+ * 1. FIRST PAINT IS REAL COPY. Never a skeleton, never an empty frame. The
8
+ * client starts from SSR `/content` or a required `fallback`.
9
+ * 2. COPY CHANGES AT MOST ONCE. Text a reader may already be mid-sentence on
10
+ * is never wiped and retyped. The stream lands as ONE deliberate rewrite.
11
+ * 3. TEXT ON THE WIRE IS TEXT WE CAN SHOW. The first CHUNK takes the canvas,
12
+ * whatever the `*-start` called itself. The delivery `mode` is no longer
13
+ * consulted for hold release — an earlier revision released on
14
+ * `mode: "generate"` and kept holding on `replay`, which re-derived from
15
+ * a label what the arrival of actual text proves directly. A replay's
16
+ * single flush still paints once, because one flush is genuinely what
17
+ * arrived — not because it was held back.
18
+ *
19
+ * The hold is the mechanism that reconciles rules 1 and 2. When the canvas
20
+ * already shows copy the stream is about to replace, that copy is a FALLBACK,
21
+ * not a first draft: showing it and then yanking it is worse than briefly
22
+ * showing the art alone. So the canvas holds blank on the art for a budget —
23
+ * a budget for the STORED FALLBACK, not for the stream: the fallback is
24
+ * allowed on screen only once the stream has failed or run out the budget
25
+ * without delivering. A fast stream types the real copy straight in; a slow
26
+ * one gets the fallback back, and any later copy arrives as the single
27
+ * permitted rewrite.
28
+ *
29
+ * Reference implementation (the iframe shell, which must stay behaviourally
30
+ * identical): `mosaic/services/rendererService.ts`, streamInitialCopyCreative.
31
+ */
32
+ import { CONTENT_SOURCES } from "./wire.js";
33
+ import { safeActionUrl } from "./safeUrl.js";
34
+ export const COPY_FIELDS = ["greeting", "headline", "body", "cta"];
35
+ /**
36
+ * How long the canvas will hold blank waiting for the stream, once the stream
37
+ * has proven it is alive.
38
+ *
39
+ * Armed against the STREAM, not the mount: the budget is for the connection
40
+ * that was just opened. The shell's earlier clock started before the transport
41
+ * had even connected, so on a slow link it expired mid-connect and painted
42
+ * stored copy that the live copy then overwrote — fallback churn nobody asked
43
+ * for, and a rule-2 violation caused by the timer meant to protect rule 2.
44
+ */
45
+ export const COPY_HOLD_MS = 2500;
46
+ function emptyField(text = "", source = "static") {
47
+ return { text, source, mode: null, typing: false, settled: false };
48
+ }
49
+ /**
50
+ * Owns one island's copy for one page view. Fed wire events; emits states.
51
+ *
52
+ * Pure with respect to the DOM: it decides WHAT should be on screen and when,
53
+ * and never touches how. That is what lets the same choreography drive React,
54
+ * the iframe shell, and anything else without three copies of these rules.
55
+ */
56
+ export class CopyChoreographer {
57
+ options;
58
+ state;
59
+ listeners = new Set();
60
+ holdMs;
61
+ setTimer;
62
+ clearTimer;
63
+ /**
64
+ * Copy pulled off the canvas when the hold armed. Alive until the stream
65
+ * TERMINATES (`done`/`failed`), not merely until the hold releases: any
66
+ * field the stream never delivers is restored from here, because a field
67
+ * the stream never took must end on its fallback — never on the blank the
68
+ * hold itself painted.
69
+ */
70
+ heldCopy = null;
71
+ holdTimer = null;
72
+ /**
73
+ * The budget may restart ONCE per hold episode. Without a cap, a server
74
+ * emitting a `*-start` per field would extend the blank window by a full
75
+ * budget per field — proof of life is proof once, and a blank canvas is a
76
+ * cost the visitor pays, so it is not for sale twice.
77
+ */
78
+ holdRestarted = false;
79
+ /** Text accumulated from the wire while the hold is still on. */
80
+ pending = {};
81
+ /** True once the machine has committed a change to the first paint. */
82
+ rewriteCommitted = false;
83
+ /**
84
+ * Fields whose on-canvas text is restored fallback a reader can see. Live
85
+ * deltas must not type over these — they buffer off-canvas and the field
86
+ * lands ONCE, composed, at its `*-done`. Typing is an entrance for a blank
87
+ * field, never a treatment for text someone is reading.
88
+ */
89
+ fallbackOnCanvas = new Set();
90
+ /**
91
+ * Fields the stream has closed with a `*-done`, including closes that
92
+ * arrived while the hold was still on and so never reached the canvas at the
93
+ * time. Consulted when the copy finally paints, so `settled` describes the
94
+ * field rather than the route it took.
95
+ */
96
+ closed = new Set();
97
+ /** Set by dispose(): async completions (image decode) must not patch after it. */
98
+ disposed = false;
99
+ constructor(options = {}) {
100
+ this.options = options;
101
+ this.holdMs = options.holdMs ?? COPY_HOLD_MS;
102
+ this.setTimer = options.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
103
+ this.clearTimer =
104
+ options.clearTimeout ?? ((handle) => clearTimeout(handle));
105
+ const fields = {};
106
+ for (const field of COPY_FIELDS) {
107
+ fields[field] = emptyField(options.initial?.[field] ?? "");
108
+ }
109
+ this.state = {
110
+ phase: "idle",
111
+ fields,
112
+ theme: null,
113
+ layout: null,
114
+ experienceManifest: null,
115
+ backgroundImage: null,
116
+ sequence: null,
117
+ videoUrl: null,
118
+ ctaStyle: null,
119
+ ctaAction: null,
120
+ meta: null,
121
+ contentSource: "static",
122
+ degraded: null,
123
+ variantId: null,
124
+ error: null,
125
+ rewrites: 0,
126
+ };
127
+ }
128
+ getState() {
129
+ return this.state;
130
+ }
131
+ subscribe(listener) {
132
+ this.listeners.add(listener);
133
+ return () => this.listeners.delete(listener);
134
+ }
135
+ emit() {
136
+ for (const listener of this.listeners) {
137
+ try {
138
+ listener(this.state);
139
+ }
140
+ catch {
141
+ // Swallowed on purpose. A subscriber throwing is the SUBSCRIBER's bug;
142
+ // letting it propagate would tear down the stream for every other
143
+ // listener and break the customer's page from inside a render effect.
144
+ // Same isolation the audio emitter applies (audio.ts createEmitter).
145
+ }
146
+ }
147
+ }
148
+ patch(next) {
149
+ this.state = { ...this.state, ...next };
150
+ this.emit();
151
+ }
152
+ patchField(field, next) {
153
+ this.state = {
154
+ ...this.state,
155
+ fields: { ...this.state.fields, [field]: { ...this.state.fields[field], ...next } },
156
+ };
157
+ this.emit();
158
+ }
159
+ hasInitialCopy() {
160
+ return COPY_FIELDS.some((field) => this.state.fields[field].text.trim() !== "");
161
+ }
162
+ /**
163
+ * Call when the stream request has been opened.
164
+ *
165
+ * Arms the hold if — and only if — there is fallback copy on the canvas that
166
+ * has NOT already been read. Copy that has been on screen since first paint
167
+ * stays put: it is not a placeholder, and hiding it would be the mid-read
168
+ * wipe this whole machine exists to prevent.
169
+ */
170
+ streamOpened() {
171
+ if (this.state.phase !== "idle")
172
+ return;
173
+ if (this.hasInitialCopy() && !this.options.initialIsOnScreen) {
174
+ this.heldCopy = {};
175
+ this.holdRestarted = false;
176
+ const blanked = {};
177
+ for (const field of COPY_FIELDS) {
178
+ this.heldCopy[field] = this.state.fields[field].text;
179
+ blanked[field] = { ...this.state.fields[field], text: "" };
180
+ }
181
+ this.state = { ...this.state, phase: "holding", fields: blanked };
182
+ this.holdTimer = this.setTimer(() => this.onHoldExpired(), this.holdMs);
183
+ this.emit();
184
+ return;
185
+ }
186
+ // Nothing to HOLD: either the canvas is genuinely blank (so live typing IS
187
+ // the entrance) or the copy is already being read (so it stays).
188
+ //
189
+ // "Stays" has to mean stays INTACT. Copy on screen since first paint is
190
+ // exactly the text rule 2 protects, so every non-empty field is marked as
191
+ // fallback-on-canvas: live deltas then buffer off-screen and the field
192
+ // lands ONCE, composed, at its `*-done` — or never, if the stream dies
193
+ // first. Without this the SDK types a half sentence over a paragraph
194
+ // someone is reading and strands them there when the stream fails, which
195
+ // is precisely what the iframe shell refuses to do (`!canvasHadCopy`).
196
+ for (const field of COPY_FIELDS) {
197
+ if (this.state.fields[field].text.trim() !== "")
198
+ this.fallbackOnCanvas.add(field);
199
+ }
200
+ this.patch({ phase: "live" });
201
+ }
202
+ /**
203
+ * Hold expired without the stream taking the canvas.
204
+ *
205
+ * Prefer whatever the stream actually delivered; the stored copy is the last
206
+ * resort, never a first draft.
207
+ */
208
+ onHoldExpired() {
209
+ this.holdTimer = null;
210
+ if (this.state.phase !== "holding")
211
+ return;
212
+ if (COPY_FIELDS.some((field) => (this.pending[field] ?? "") !== "")) {
213
+ this.releaseHoldToLive(true);
214
+ return;
215
+ }
216
+ this.restoreHeld();
217
+ }
218
+ /**
219
+ * The stream is over and the canvas is final.
220
+ *
221
+ * `readSseStream` keeps draining after a terminal event, and the parser's
222
+ * flush can emit a trailing frame, so late copy events are reachable rather
223
+ * than theoretical. Without this guard a post-`failed` chunk left
224
+ * `typing: true` forever — a caret animating on a dead stream — and a
225
+ * post-`done` chunk+done replaced settled text WITHOUT incrementing
226
+ * `rewrites`, so the doctrine counter read 1 while the reader saw two
227
+ * changes. Everything after the terminal event is dropped.
228
+ */
229
+ isTerminal() {
230
+ return this.state.phase === "settled" || this.state.phase === "failed";
231
+ }
232
+ clearHoldTimer() {
233
+ if (this.holdTimer !== null) {
234
+ this.clearTimer(this.holdTimer);
235
+ this.holdTimer = null;
236
+ }
237
+ }
238
+ /**
239
+ * Hand the canvas to the live stream: what has accumulated paints at once,
240
+ * so the rest can stream in behind it.
241
+ *
242
+ * `restoreUndelivered` distinguishes the two callers. On a chunk-driven
243
+ * release the stream is alive and delivering, so undelivered fields stay
244
+ * blank — typing into a blank canvas is their entrance, and `heldCopy`
245
+ * outlives the hold so `done`/`failed` restore any field the stream never
246
+ * takes. On budget EXPIRY the stream has sputtered: whatever it delivered
247
+ * paints, and every undelivered field gets its fallback back now, because
248
+ * blank is never the resting state the budget is allowed to buy.
249
+ */
250
+ releaseHoldToLive(restoreUndelivered) {
251
+ this.clearHoldTimer();
252
+ const fields = { ...this.state.fields };
253
+ let paintedStreamText = false;
254
+ for (const field of COPY_FIELDS) {
255
+ const text = this.pending[field];
256
+ if (text !== undefined && text !== "") {
257
+ fields[field] = { ...fields[field], text, settled: this.closed.has(field) };
258
+ this.fallbackOnCanvas.delete(field);
259
+ if (text !== (this.heldCopy?.[field] ?? ""))
260
+ paintedStreamText = true;
261
+ continue;
262
+ }
263
+ if (!restoreUndelivered)
264
+ continue;
265
+ const held = this.heldCopy?.[field];
266
+ if (held !== undefined && held !== "") {
267
+ fields[field] = { ...fields[field], text: held };
268
+ this.fallbackOnCanvas.add(field);
269
+ }
270
+ }
271
+ // Restoring fallback is not a rewrite — nothing the reader saw changed
272
+ // versus first paint. Stream text landing is the one rewrite.
273
+ if (paintedStreamText)
274
+ this.commitRewrite();
275
+ this.state = { ...this.state, phase: "live", fields };
276
+ this.emit();
277
+ }
278
+ /** Budget spent with nothing delivered: the fallback comes back, composed. */
279
+ restoreHeld() {
280
+ this.clearHoldTimer();
281
+ if (!this.heldCopy) {
282
+ this.patch({ phase: "live" });
283
+ return;
284
+ }
285
+ const fields = { ...this.state.fields };
286
+ for (const field of COPY_FIELDS) {
287
+ const text = this.heldCopy[field];
288
+ if (text !== undefined)
289
+ fields[field] = { ...fields[field], text };
290
+ if (text !== undefined && text !== "")
291
+ this.fallbackOnCanvas.add(field);
292
+ }
293
+ this.heldCopy = null;
294
+ this.state = { ...this.state, phase: "live", fields };
295
+ this.emit();
296
+ }
297
+ /**
298
+ * Record that the first paint has been replaced. Restoring the SAME fallback
299
+ * text is not a rewrite — nothing on screen changed — which is why every
300
+ * caller commits only when STREAM text actually painted, and restoreHeld
301
+ * never commits at all.
302
+ */
303
+ commitRewrite() {
304
+ if (this.rewriteCommitted)
305
+ return;
306
+ this.rewriteCommitted = true;
307
+ this.state = { ...this.state, rewrites: this.state.rewrites + 1 };
308
+ }
309
+ /**
310
+ * Terminal honesty pass over provenance.
311
+ *
312
+ * A field still in `fallbackOnCanvas` when the stream ends is showing the
313
+ * customer's AUTHORED text: its buffered deltas never landed. Its `source`
314
+ * still says `generated` because `*-start` declared an intent the stream
315
+ * then failed to deliver — and `StreamText` marks on `field.source`, so
316
+ * leaving it would stamp "adapted for you" on copy nobody adapted.
317
+ *
318
+ * The mark must describe what is on screen, not what the server meant to
319
+ * send. A missing mark on adapted copy under-claims; a mark on authored copy
320
+ * is a false claim, and that is the worse of the two.
321
+ *
322
+ * `fallbackOnCanvas` is NOT the whole set. A chunk-driven release leaves
323
+ * undelivered fields blank — restoring them mid-stream would stage the
324
+ * wipe-and-retype this machine exists to prevent — so they never enter that
325
+ * set, and are instead restored from `heldCopy` by the terminal handlers.
326
+ * That is the likeliest degradation there is: the server opens a field,
327
+ * then times out. Demoting only `fallbackOnCanvas` left exactly that path
328
+ * rendering "adapted for you" over the customer's own sentence.
329
+ *
330
+ * So callers pass the fields they restored, and both sets are demoted.
331
+ */
332
+ demoteUndeliveredProvenance(fields, restoredFromHeld = []) {
333
+ for (const field of new Set([...this.fallbackOnCanvas, ...restoredFromHeld])) {
334
+ fields[field] = { ...fields[field], source: "static", mode: null };
335
+ }
336
+ }
337
+ /** True once any field on canvas actually carries adapted copy. */
338
+ hasAdaptedCopyOnCanvas(fields) {
339
+ return COPY_FIELDS.some((field) => fields[field].text.trim() !== "" && fields[field].source !== "static");
340
+ }
341
+ // --- wire events -----------------------------------------------------------
342
+ /**
343
+ * A field opened. Proof of life, and provenance — nothing more.
344
+ *
345
+ * The canvas changes hands on the first CHUNK, not here: the delivery mode
346
+ * is recorded on the field but no longer consulted for hold release. All a
347
+ * start does while holding is RESTART the fallback budget, because the
348
+ * stream has just proven it is alive and the old budget was measuring a
349
+ * connection that had not connected yet.
350
+ */
351
+ copyStart(field, mode, source) {
352
+ // A caret is only honest when deltas will actually land on canvas; deltas
353
+ // aimed at visible fallback copy buffer off-canvas instead.
354
+ const willType = mode === "generate" && !this.fallbackOnCanvas.has(field);
355
+ this.patchField(field, { mode, source, typing: willType, settled: false });
356
+ this.promoteContentSource(source);
357
+ if (this.state.phase !== "holding")
358
+ return;
359
+ // One restart buys the freshly-proven stream a full budget; further
360
+ // starts do not extend the blank window past that cap.
361
+ if (this.holdRestarted)
362
+ return;
363
+ this.holdRestarted = true;
364
+ this.clearHoldTimer();
365
+ this.holdTimer = this.setTimer(() => this.onHoldExpired(), this.holdMs);
366
+ }
367
+ /**
368
+ * A delta landed. Text on the wire is text we can show: the first chunk
369
+ * takes the canvas away from the pending fallback, whatever the start
370
+ * called itself. While the field shows restored fallback copy a reader can
371
+ * see, deltas still buffer off-canvas — that text changes ONCE, composed,
372
+ * at `*-done`, never delta by delta.
373
+ */
374
+ copyChunk(field, text) {
375
+ if (this.isTerminal())
376
+ return;
377
+ this.pending[field] = (this.pending[field] ?? "") + text;
378
+ if (this.state.phase === "holding") {
379
+ // The release paints everything pending, this chunk included.
380
+ this.releaseHoldToLive(false);
381
+ return;
382
+ }
383
+ if (this.fallbackOnCanvas.has(field))
384
+ return;
385
+ // Commit BEFORE the emitting patch: subscribers must see the counter and
386
+ // the text move together, not the counter one event late.
387
+ this.commitRewrite();
388
+ this.patchField(field, { text: this.pending[field], typing: true });
389
+ }
390
+ /**
391
+ * A field closed. `text` is authoritative — a client that missed chunks can
392
+ * paint this alone, which is what makes chunk loss a cosmetic problem
393
+ * instead of a correctness one.
394
+ */
395
+ copyDone(field, text) {
396
+ if (this.isTerminal())
397
+ return;
398
+ this.pending[field] = text;
399
+ // Record the close BEFORE the hold's early return. A replay that lands
400
+ // entirely inside the hold used to leave `settled: false` forever: this
401
+ // returned before patching, and the terminal handlers only cleared
402
+ // `typing`. `settled` is public API and a documented styling hook, so a
403
+ // field whose text is final must say so however it arrived.
404
+ this.closed.add(field);
405
+ if (this.state.phase === "holding")
406
+ return;
407
+ this.fallbackOnCanvas.delete(field);
408
+ // Landing text identical to what is on canvas changes nothing a reader
409
+ // can see, so it spends no rewrite.
410
+ if (text !== this.state.fields[field].text)
411
+ this.commitRewrite();
412
+ this.patchField(field, { text, typing: false, settled: true });
413
+ }
414
+ themeReady(colors, sources) {
415
+ this.patch({ theme: { colors, sources } });
416
+ }
417
+ layoutReady(layout, manifest) {
418
+ this.patch({
419
+ layout,
420
+ ...(manifest !== undefined ? { experienceManifest: manifest } : {}),
421
+ });
422
+ }
423
+ /**
424
+ * Absence of this event means KEEP THE BAKED THEME WASH. It does not mean
425
+ * clear the art layer — the server omits it when there is no creative image,
426
+ * and a client that cleared on absence would blank the hero on exactly the
427
+ * campaigns that never had a picture.
428
+ *
429
+ * The patch waits for the browser to DECODE the image first: consumers
430
+ * render this state directly, and a backgroundImage that lands before
431
+ * pixels exist paints the already-flipped theme against the old (or blank)
432
+ * art — the contrast tear. On failure the patch still lands (the consumer's
433
+ * own loading treatment owns that case); outside a browser it is immediate.
434
+ */
435
+ backgroundReady(bgImage) {
436
+ if (!bgImage)
437
+ return;
438
+ // The decode wait is async: by the time onload fires the consumer may
439
+ // have disposed this choreographer, and patching then would resurrect
440
+ // state the owner already tore down.
441
+ const apply = () => {
442
+ if (this.disposed)
443
+ return;
444
+ this.patch({ backgroundImage: bgImage });
445
+ };
446
+ if (typeof Image === "undefined") {
447
+ apply();
448
+ return;
449
+ }
450
+ const img = new Image();
451
+ img.onload = () => {
452
+ if (img.decode)
453
+ img.decode().then(apply, apply);
454
+ else
455
+ apply();
456
+ };
457
+ img.onerror = apply;
458
+ img.src = bgImage;
459
+ }
460
+ sequenceReady(frames, heroIndex, fps) {
461
+ if (frames.length)
462
+ this.patch({ sequence: { frames, heroIndex, fps } });
463
+ }
464
+ videoReady(videoUrl) {
465
+ if (videoUrl)
466
+ this.patch({ videoUrl });
467
+ }
468
+ ctaStyleReady(style) {
469
+ this.patch({ ctaStyle: style });
470
+ }
471
+ ctaReady(text, style, action) {
472
+ this.patch({
473
+ ctaStyle: style,
474
+ ...(action ? { ctaAction: action } : {}),
475
+ });
476
+ if (text)
477
+ this.copyDone("cta", text);
478
+ }
479
+ /**
480
+ * Terminal. Anything still held paints now, once, composed — the last
481
+ * chance for the stream's copy to land as a single deliberate change rather
482
+ * than being dropped because the budget happened to outlive the stream.
483
+ */
484
+ done(meta) {
485
+ // Terminal is terminal: a trailing `done` after `error` (or a duplicate
486
+ // `done` — the parser's flush can re-emit a trailing frame) must not
487
+ // re-open a settled stream or rebrand a failure as a success.
488
+ if (this.isTerminal())
489
+ return;
490
+ if (this.state.phase === "holding") {
491
+ if (COPY_FIELDS.some((field) => (this.pending[field] ?? "") !== "")) {
492
+ // The blank-field restore below handles the undelivered fields.
493
+ this.releaseHoldToLive(false);
494
+ }
495
+ else {
496
+ this.restoreHeld();
497
+ }
498
+ }
499
+ this.clearHoldTimer();
500
+ const fields = { ...this.state.fields };
501
+ const restored = [];
502
+ for (const field of COPY_FIELDS) {
503
+ fields[field] = {
504
+ ...fields[field],
505
+ typing: false,
506
+ settled: fields[field].settled || this.closed.has(field),
507
+ };
508
+ // A field the stream never delivered ends on its fallback, never on
509
+ // the blank the hold painted. Not a rewrite: nothing the reader saw
510
+ // changed versus first paint.
511
+ const held = this.heldCopy?.[field];
512
+ if (fields[field].text === "" && held !== undefined && held !== "") {
513
+ fields[field] = { ...fields[field], text: held };
514
+ restored.push(field);
515
+ }
516
+ }
517
+ this.heldCopy = null;
518
+ this.demoteUndeliveredProvenance(fields, restored);
519
+ this.fallbackOnCanvas.clear();
520
+ const declared = typeof meta.contentSource === "string" &&
521
+ CONTENT_SOURCES.includes(meta.contentSource)
522
+ ? meta.contentSource
523
+ : null;
524
+ this.state = {
525
+ ...this.state,
526
+ phase: "settled",
527
+ fields,
528
+ meta,
529
+ variantId: typeof meta.variantId === "string" ? meta.variantId : this.state.variantId,
530
+ // The action renders as an <a href>; an unprovable scheme stays out of
531
+ // state and the CTA degrades to a plain button.
532
+ ctaAction: safeActionUrl(typeof meta.ctaAction === "string" ? meta.ctaAction : undefined) ?? this.state.ctaAction,
533
+ degraded: typeof meta.degraded === "string" ? meta.degraded : null,
534
+ // The server's terminal word on provenance beats the per-field
535
+ // observation; it is the value the server itself will stand behind.
536
+ contentSource: declared ?? this.state.contentSource,
537
+ };
538
+ this.emit();
539
+ }
540
+ /**
541
+ * The stream failed.
542
+ *
543
+ * Whatever is painted STAYS painted. An error is never a reason to blank
544
+ * copy a reader is mid-sentence on — the worst honest outcome is that the
545
+ * creative simply stopped adapting, which looks like a normal page.
546
+ */
547
+ failed(message) {
548
+ // Terminal is terminal: a late `error` after `done` must not rebrand a
549
+ // settled stream as a failure — the copy the reader finished with was
550
+ // real and complete.
551
+ if (this.isTerminal())
552
+ return;
553
+ if (this.state.phase === "holding")
554
+ this.restoreHeld();
555
+ this.clearHoldTimer();
556
+ const fields = { ...this.state.fields };
557
+ const restored = [];
558
+ for (const field of COPY_FIELDS) {
559
+ fields[field] = { ...fields[field], typing: false };
560
+ // A blank the machine itself painted must not survive the stream that
561
+ // justified it. Painted copy — including partial live copy — stays; a
562
+ // field still blank when the transport dies gets its fallback back.
563
+ const held = this.heldCopy?.[field];
564
+ if (fields[field].text === "" && held !== undefined && held !== "") {
565
+ fields[field] = { ...fields[field], text: held };
566
+ restored.push(field);
567
+ }
568
+ }
569
+ this.heldCopy = null;
570
+ this.demoteUndeliveredProvenance(fields, restored);
571
+ this.fallbackOnCanvas.clear();
572
+ // No server word to defer to here, so the stream-level value is recomputed
573
+ // from what actually reached the canvas. A failed stream that delivered
574
+ // nothing readable leaves a page of authored copy, and the analytics that
575
+ // reads `contentSource` should say so rather than reporting an adaptation
576
+ // the visitor never saw.
577
+ const contentSource = this.hasAdaptedCopyOnCanvas(fields) ? this.state.contentSource : "static";
578
+ this.state = { ...this.state, phase: "failed", fields, contentSource, error: message };
579
+ this.emit();
580
+ }
581
+ /** Release timers. Safe to call twice. */
582
+ dispose() {
583
+ this.disposed = true;
584
+ this.clearHoldTimer();
585
+ this.listeners.clear();
586
+ }
587
+ promoteContentSource(source) {
588
+ const rank = { static: 0, cached: 1, generated: 2 };
589
+ if (rank[source] > rank[this.state.contentSource]) {
590
+ this.state = { ...this.state, contentSource: source };
591
+ }
592
+ }
593
+ }
594
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,eAAe,EAAqC,MAAM,WAAW,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI7C,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAEzF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AAyDjC,SAAS,UAAU,CAAC,IAAI,GAAG,EAAE,EAAE,SAAwB,QAAQ;IAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AACrE,CAAC;AAwBD;;;;;;GAMG;AACH,MAAM,OAAO,iBAAiB;IA4CC;IA3CrB,KAAK,CAAgB;IACrB,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IACvB,MAAM,CAAS;IACf,QAAQ,CAA0C;IAClD,UAAU,CAA4B;IAEvD;;;;;;OAMG;IACK,QAAQ,GAA8C,IAAI,CAAC;IAC3D,SAAS,GAAY,IAAI,CAAC;IAClC;;;;;OAKG;IACK,aAAa,GAAG,KAAK,CAAC;IAC9B,iEAAiE;IACzD,OAAO,GAAuC,EAAE,CAAC;IACzD,uEAAuE;IAC/D,gBAAgB,GAAG,KAAK,CAAC;IACjC;;;;;OAKG;IACK,gBAAgB,GAAG,IAAI,GAAG,EAAa,CAAC;IAChD;;;;;OAKG;IACK,MAAM,GAAG,IAAI,GAAG,EAAa,CAAC;IACtC,kFAAkF;IAC1E,QAAQ,GAAG,KAAK,CAAC;IAEzB,YAA6B,UAAgC,EAAE;QAAlC,YAAO,GAAP,OAAO,CAA2B;QAC7D,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,YAAY,CAAC;QAC7C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,UAAU;YACb,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,MAAuC,CAAC,CAAC,CAAC;QAE9F,MAAM,MAAM,GAAG,EAAmC,CAAC;QACnD,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,KAAK,GAAG;YACX,KAAK,EAAE,MAAM;YACb,MAAM;YACN,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,IAAI;YACZ,kBAAkB,EAAE,IAAI;YACxB,eAAe,EAAE,IAAI;YACrB,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,IAAI;YACV,aAAa,EAAE,QAAQ;YACvB,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,KAAK,EAAE,IAAI;YACX,QAAQ,EAAE,CAAC;SACZ,CAAC;IACJ,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,SAAS,CAAC,QAAkB;QAC1B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IAEO,IAAI;QACV,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,uEAAuE;gBACvE,kEAAkE;gBAClE,sEAAsE;gBACtE,qEAAqE;YACvE,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAA4B;QACxC,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,EAAE,CAAC;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAEO,UAAU,CAAC,KAAgB,EAAE,IAAyB;QAC5D,IAAI,CAAC,KAAK,GAAG;YACX,GAAG,IAAI,CAAC,KAAK;YACb,MAAM,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE;SACpF,CAAC;QACF,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAEO,cAAc;QACpB,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClF,CAAC;IAED;;;;;;;OAOG;IACH,YAAY;QACV,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM;YAAE,OAAO;QAExC,IAAI,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;YAC7D,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,MAAM,OAAO,GAAG,EAAmC,CAAC;YACpD,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;gBAChC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;gBACrD,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;YAC7D,CAAC;YACD,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;YAClE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACxE,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO;QACT,CAAC;QAED,2EAA2E;QAC3E,iEAAiE;QACjE,EAAE;QACF,wEAAwE;QACxE,0EAA0E;QAC1E,uEAAuE;QACvE,uEAAuE;QACvE,qEAAqE;QACrE,yEAAyE;QACzE,uEAAuE;QACvE,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,CAAC;IAED;;;;;OAKG;IACK,aAAa;QACnB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO;QAE3C,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC;YACpE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,IAAI,CAAC,WAAW,EAAE,CAAC;IACrB,CAAC;IAED;;;;;;;;;;OAUG;IACK,UAAU;QAChB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC;IACzE,CAAC;IAEO,cAAc;QACpB,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAChC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACK,iBAAiB,CAAC,kBAA2B;QACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QAEtB,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACxC,IAAI,iBAAiB,GAAG,KAAK,CAAC;QAC9B,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACjC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;gBACtC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5E,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACpC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;oBAAE,iBAAiB,GAAG,IAAI,CAAC;gBACtE,SAAS;YACX,CAAC;YACD,IAAI,CAAC,kBAAkB;gBAAE,SAAS;YAClC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;gBACtC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;gBACjD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QACD,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,iBAAiB;YAAE,IAAI,CAAC,aAAa,EAAE,CAAC;QAC5C,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,8EAA8E;IACtE,WAAW;QACjB,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACxC,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClC,IAAI,IAAI,KAAK,SAAS;gBAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;YACnE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE;gBAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACK,aAAa;QACnB,IAAI,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAClC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;IACpE,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACK,2BAA2B,CACjC,MAAqC,EACrC,mBAAwC,EAAE;QAE1C,KAAK,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC;YAC7E,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACrE,CAAC;IACH,CAAC;IAED,mEAAmE;IAC3D,sBAAsB,CAAC,MAAqC;QAClE,OAAO,WAAW,CAAC,IAAI,CACrB,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,QAAQ,CACjF,CAAC;IACJ,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;OAQG;IACH,SAAS,CAAC,KAAgB,EAAE,IAAqB,EAAE,MAAqB;QACtE,0EAA0E;QAC1E,4DAA4D;QAC5D,MAAM,QAAQ,GAAG,IAAI,KAAK,UAAU,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1E,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3E,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAElC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO;QAC3C,oEAAoE;QACpE,uDAAuD;QACvD,IAAI,IAAI,CAAC,aAAa;YAAE,OAAO;QAC/B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAE1B,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1E,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAgB,EAAE,IAAY;QACtC,IAAI,IAAI,CAAC,UAAU,EAAE;YAAE,OAAO;QAC9B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QAEzD,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACnC,8DAA8D;YAC9D,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,OAAO;QAC7C,yEAAyE;QACzE,0DAA0D;QAC1D,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;IAED;;;;OAIG;IACH,QAAQ,CAAC,KAAgB,EAAE,IAAY;QACrC,IAAI,IAAI,CAAC,UAAU,EAAE;YAAE,OAAO;QAC9B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;QAC3B,uEAAuE;QACvE,wEAAwE;QACxE,mEAAmE;QACnE,wEAAwE;QACxE,4DAA4D;QAC5D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO;QAC3C,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACpC,uEAAuE;QACvE,oCAAoC;QACpC,IAAI,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI;YAAE,IAAI,CAAC,aAAa,EAAE,CAAC;QACjE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,UAAU,CAAC,MAA8B,EAAE,OAA+B;QACxE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,WAAW,CAAC,MAAc,EAAE,QAAkB;QAC5C,IAAI,CAAC,KAAK,CAAC;YACT,MAAM;YACN,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpE,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,OAAe;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,sEAAsE;QACtE,sEAAsE;QACtE,qCAAqC;QACrC,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAO;YAC1B,IAAI,CAAC,KAAK,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC,CAAC;QACF,IAAI,OAAO,KAAK,KAAK,WAAW,EAAE,CAAC;YACjC,KAAK,EAAE,CAAC;YACR,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE;YAChB,IAAI,GAAG,CAAC,MAAM;gBAAE,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;;gBAC3C,KAAK,EAAE,CAAC;QACf,CAAC,CAAC;QACF,GAAG,CAAC,OAAO,GAAG,KAAK,CAAC;QACpB,GAAG,CAAC,GAAG,GAAG,OAAO,CAAC;IACpB,CAAC;IAED,aAAa,CAAC,MAAgB,EAAE,SAAiB,EAAE,GAAW;QAC5D,IAAI,MAAM,CAAC,MAAM;YAAE,IAAI,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,UAAU,CAAC,QAAgB;QACzB,IAAI,QAAQ;YAAE,IAAI,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,aAAa,CAAC,KAAc;QAC1B,IAAI,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,QAAQ,CAAC,IAAY,EAAE,KAAc,EAAE,MAAe;QACpD,IAAI,CAAC,KAAK,CAAC;YACT,QAAQ,EAAE,KAAK;YACf,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzC,CAAC,CAAC;QACH,IAAI,IAAI;YAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,IAA6B;QAChC,wEAAwE;QACxE,qEAAqE;QACrE,8DAA8D;QAC9D,IAAI,IAAI,CAAC,UAAU,EAAE;YAAE,OAAO;QAC9B,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC;gBACpE,gEAAgE;gBAChE,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,cAAc,EAAE,CAAC;QAEtB,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAgB,EAAE,CAAC;QACjC,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,CAAC,GAAG;gBACd,GAAG,MAAM,CAAC,KAAK,CAAC;gBAChB,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;aACzD,CAAC;YACF,oEAAoE;YACpE,oEAAoE;YACpE,8BAA8B;YAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;gBACnE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;gBACjD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,2BAA2B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACnD,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAE9B,MAAM,QAAQ,GACZ,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ;YACrC,eAAqC,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC;YACjE,CAAC,CAAE,IAAI,CAAC,aAA+B;YACvC,CAAC,CAAC,IAAI,CAAC;QACX,IAAI,CAAC,KAAK,GAAG;YACX,GAAG,IAAI,CAAC,KAAK;YACb,KAAK,EAAE,SAAS;YAChB,MAAM;YACN,IAAI;YACJ,SAAS,EAAE,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS;YACrF,uEAAuE;YACvE,gDAAgD;YAChD,SAAS,EAAE,aAAa,CACtB,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAChE,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS;YACzB,QAAQ,EAAE,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;YAClE,+DAA+D;YAC/D,oEAAoE;YACpE,aAAa,EAAE,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa;SACpD,CAAC;QACF,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,OAAe;QACpB,uEAAuE;QACvE,sEAAsE;QACtE,qBAAqB;QACrB,IAAI,IAAI,CAAC,UAAU,EAAE;YAAE,OAAO;QAC9B,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,WAAW,EAAE,CAAC;QACvD,IAAI,CAAC,cAAc,EAAE,CAAC;QAEtB,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAgB,EAAE,CAAC;QACjC,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;YACpD,sEAAsE;YACtE,sEAAsE;YACtE,oEAAoE;YACpE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;gBACnE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;gBACjD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,2BAA2B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACnD,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,0EAA0E;QAC1E,yBAAyB;QACzB,MAAM,aAAa,GAAG,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC;QAChG,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QACvF,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,0CAA0C;IAC1C,OAAO;QACL,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAEO,oBAAoB,CAAC,MAAqB;QAChD,MAAM,IAAI,GAAkC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;QACnF,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;YAClD,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;QACxD,CAAC;IACH,CAAC;CACF"}
package/dist/text.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Wire text arrives from catalog systems that store rich text — Shopify
3
+ * product descriptions are `body_html`, and campaign tooling pastes markdown.
4
+ * The SDK never renders wire content as HTML (that would hand every catalog
5
+ * entry a script slot), so display text is normalized to plain text instead:
6
+ * tags stripped, block boundaries kept as spaces, the handful of entities
7
+ * that actually occur decoded.
8
+ */
9
+ export declare function plainText(value: string): string;
10
+ //# sourceMappingURL=text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkB/C"}
package/dist/text.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Wire text arrives from catalog systems that store rich text — Shopify
3
+ * product descriptions are `body_html`, and campaign tooling pastes markdown.
4
+ * The SDK never renders wire content as HTML (that would hand every catalog
5
+ * entry a script slot), so display text is normalized to plain text instead:
6
+ * tags stripped, block boundaries kept as spaces, the handful of entities
7
+ * that actually occur decoded.
8
+ */
9
+ /* ponytail: regex strip, not an HTML parser — fine for catalog copy; revisit
10
+ only if real payloads show nested/malformed markup it mangles. */
11
+ export function plainText(value) {
12
+ if (!value)
13
+ return "";
14
+ return (value
15
+ // Block-level closers and breaks become spaces so paragraphs don't fuse.
16
+ .replace(/<(?:br|\/p|\/div|\/h[1-6]|\/li|\/tr|\/ul|\/ol)\b[^>]*>/gi, " ")
17
+ .replace(/<[^>]+>/g, "")
18
+ .replace(/&nbsp;/gi, " ")
19
+ .replace(/&amp;/gi, "&")
20
+ .replace(/&lt;/gi, "<")
21
+ .replace(/&gt;/gi, ">")
22
+ .replace(/&quot;/gi, '"')
23
+ .replace(/&#0?39;/g, "'")
24
+ // Stray markdown heading runs seen in real Shopify descriptions.
25
+ .replace(/#{2,}\s*/g, "")
26
+ .replace(/\s+/g, " ")
27
+ .trim());
28
+ }
29
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;oEACoE;AACpE,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,CACL,KAAK;QACH,yEAAyE;SACxE,OAAO,CAAC,0DAA0D,EAAE,GAAG,CAAC;SACxE,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;QACzB,iEAAiE;SAChE,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;SACxB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CACV,CAAC;AACJ,CAAC"}