@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SparkleTree
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @sparkletree/core
2
+
3
+ The framework-agnostic half of the SparkleTree SDK. Wire protocol, copy
4
+ choreography, theme, trust chrome, analytics, variant pinning, audio.
5
+
6
+ **This is the only thing that speaks the wire.** If you are building a React
7
+ app, install [`@sparkletree/react`](https://www.npmjs.com/package/@sparkletree/react)
8
+ instead — it depends on this. Reach for `core` directly when you are wiring up
9
+ Vue, Svelte, Solid, or plain DOM.
10
+
11
+ ```bash
12
+ npm install @sparkletree/core
13
+ ```
14
+
15
+ ## The shortest useful example
16
+
17
+ ```ts
18
+ import { openStream, decideChrome } from "@sparkletree/core";
19
+
20
+ const handle = openStream({
21
+ target: {
22
+ apiBase: "https://your-embed-api.example",
23
+ organizationId: "org_123",
24
+ campaignId: "camp_123",
25
+ island: "hero",
26
+ },
27
+ // Required. This is what the visitor reads if the network is slow, the
28
+ // campaign is paused, or generation fails. Write it as the only version.
29
+ initial: { headline: "Software that meets the moment" },
30
+ // True when that copy is ALREADY on screen (SSR, or carried across a
31
+ // navigation). It changes the choreography: copy a reader can see is never
32
+ // typed over.
33
+ initialIsOnScreen: false,
34
+ onState: (state) => {
35
+ const { marked } = decideChrome(state.contentSource, false);
36
+ render(state.fields.headline.text, marked);
37
+ },
38
+ });
39
+
40
+ const final = await handle.finished;
41
+ handle.close();
42
+ ```
43
+
44
+ `openStream` returns `{ choreographer, finished, close }`. `choreographer.getState()`
45
+ is the current state; `finished` resolves with the terminal state; `close()`
46
+ aborts the request and releases timers.
47
+
48
+ ## What you get
49
+
50
+ | Export | |
51
+ |---|---|
52
+ | `openStream` | The client. Opens the SSE stream and drives the choreographer. |
53
+ | `fetchContent` | Fetches `/content`. **Nothing calls it** — `openStream` does not, and the React path does not. Exported for consumers who want a first-paint fetch of their own. |
54
+ | `CopyChoreographer`, `COPY_FIELDS`, `COPY_HOLD_MS` | The state machine, if you want to drive it yourself. |
55
+ | `SseParser`, `readSseStream` | SSE parsing, usable standalone. |
56
+ | `decideChrome`, `viewerPrefersStandard`, `setViewerPrefersStandard`, `STANDARD_MODE_KEY` | Trust chrome decisions. |
57
+ | `applyTheme`, `themeStyle`, `tokenName`, `tokenSource`, `TOKEN_PREFIX` | Theme tokens. |
58
+ | `track`, `ImpressionRecorder`, `observeVisibility`, `sessionId` | Analytics. |
59
+ | `pageVariants`, `VariantRegistry`, `RESERVATION_TIMEOUT_MS` | Variant pinning — one page view, one variant. |
60
+ | `mountAudio`, `activePlayer`, `destroyActiveAudio` | Audio engine (no chrome). |
61
+ | `buildStreamUrl`, `buildContentUrl`, `negotiatedProtocol`, `PROTOCOL_*`, `SDK_PARAM`, `VARIANT_PARAM` | Protocol. |
62
+
63
+ ## Copy state
64
+
65
+ `state.fields.{greeting,headline,body,cta}` each carry
66
+ `{ text, source, mode, typing, settled }`, already choreographed — you render
67
+ `text` and honour `typing`/`settled` for presentation only. Alongside them:
68
+ `phase`, `error`, `rewrites`, `contentSource`, `degraded`, `variantId`,
69
+ `theme`, `layout`, `backgroundImage`, `videoUrl`, `sequence`, `ctaStyle`,
70
+ `ctaAction`, `meta`.
71
+
72
+ `phase` moves `idle → holding? → live → settled | failed`. Nothing throws and
73
+ nothing blanks: on `failed`, whatever was painted stays painted and undelivered
74
+ fields fall back to your `initial` text.
75
+
76
+ ## The rules this enforces for you
77
+
78
+ 1. **First paint is real copy.** No skeleton, and no option for one.
79
+ 2. **Copy changes at most once.** Text a reader may be mid-sentence on is never
80
+ wiped and retyped. Deltas aimed at copy already on screen buffer off-canvas
81
+ and land once, composed.
82
+ 3. **The server declares delivery mode**; the client never guesses from timing.
83
+ 4. **Adapted copy is marked.** `decideChrome` returns `marked` for `generated`
84
+ and `cached`, never for `static`. Marking authored copy as adapted is a
85
+ worse failure than not marking adapted copy.
86
+
87
+ Full documentation, including the React quickstart and the reasoning behind
88
+ each rule, is in the [SDK README](https://gitlab.com/sparkletree/sparkletree-sdk/-/blob/main/README.md).
89
+
90
+ ## Requirements
91
+
92
+ ES2022, **ESM only**, Node >= 18. `fetch`, `ReadableStream` and
93
+ `AbortController` must exist — inject `fetchImpl` if you need to substitute
94
+ them. No dependencies.
95
+
96
+ There is no `require` export condition, which is worth knowing in two places:
97
+
98
+ - **From CommonJS**, `require("@sparkletree/core")` fails on Node 18 with
99
+ `ERR_REQUIRE_ESM`. Use `await import("@sparkletree/core")`. (Node 26 loads it
100
+ either way, so this depends on your runtime, not your code.)
101
+ - **In TypeScript**, `moduleResolution: "bundler"` resolves cleanly. So does
102
+ `"node16"`/`"nodenext"` *if your own package.json has `"type": "module"`*.
103
+ Without it you get `TS1479` telling you to use a dynamic import — accurate,
104
+ not a false positive, and the most likely way a Node backend meets this
105
+ package.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Analytics: session, impression, click.
3
+ *
4
+ * Impression ownership (plan v1.1 §3.4) is the load-bearing idea. The SDK
5
+ * renders inside the customer's page, so it is the only party that knows an
6
+ * impression actually happened — the server knows only that bytes were
7
+ * requested, which is not the same thing and is spectacularly not the same
8
+ * thing behind a CDN. So every SDK request carries `sdk=1`, the server
9
+ * suppresses its own impression, and this module owns the count.
10
+ *
11
+ * Which means the rules here are not conveniences, they are the integrity of
12
+ * CVG:
13
+ *
14
+ * - ONE impression per island per page view. Not per render, not per state
15
+ * change, not per React strict-mode double-invoke.
16
+ * - VISIBILITY, not mount. A component rendered below the fold, or in a
17
+ * background tab, has not been seen.
18
+ * - the impression carries what the viewer was actually shown, including
19
+ * whether the trust chrome was on.
20
+ */
21
+ import type { ContentSource } from "./wire.js";
22
+ export type EventType = "impression" | "click" | "conversion" | "custom";
23
+ export interface AnalyticsContext {
24
+ apiBase: string;
25
+ organizationId: string;
26
+ campaignId: string;
27
+ variantId?: string;
28
+ surfaceId?: string;
29
+ }
30
+ export interface ImpressionDetail {
31
+ contentSource: ContentSource;
32
+ /** Was the adaptation mark rendered? Reported so chrome state is measurable. */
33
+ chromeMarked: boolean;
34
+ /** W8 — set when the server served a deterministic winner. */
35
+ degraded?: string | null;
36
+ island?: string;
37
+ }
38
+ export declare function sessionId(): string;
39
+ /** POST an event. Fire-and-forget: telemetry never blocks or breaks a render. */
40
+ export declare function track(context: AnalyticsContext, eventType: EventType, eventData?: Record<string, unknown>): Promise<void>;
41
+ /**
42
+ * Records at most one impression per island per page view.
43
+ *
44
+ * The guard is an instance, not a module global, so two islands on one page
45
+ * each get their own — while a single island being re-rendered, re-subscribed
46
+ * or double-invoked by React strict mode gets exactly one.
47
+ */
48
+ export declare class ImpressionRecorder {
49
+ private readonly context;
50
+ private fired;
51
+ constructor(context: AnalyticsContext);
52
+ get hasFired(): boolean;
53
+ /** Idempotent. Returns true only for the call that actually recorded. */
54
+ record(detail: ImpressionDetail): boolean;
55
+ }
56
+ export interface VisibilityOptions {
57
+ /** Fraction of the element that must be visible. */
58
+ threshold?: number;
59
+ /**
60
+ * How long it must stay that visible before it counts. A creative that
61
+ * scrolled past during a fling was not seen by anybody.
62
+ */
63
+ dwellMs?: number;
64
+ }
65
+ /**
66
+ * Fire `onVisible` once, when the element has been meaningfully on screen.
67
+ *
68
+ * Where IntersectionObserver does not exist, this fires immediately rather
69
+ * than never: under-counting every impression in older browsers would be a
70
+ * silent, systematic bias in the numbers a customer is billed and optimised
71
+ * against. Over-counting there is the lesser wrong, and it is at least
72
+ * visible.
73
+ */
74
+ export declare function observeVisibility(element: Element, onVisible: () => void, options?: VisibilityOptions): () => void;
75
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,aAAa,CAAC;IAC7B,gFAAgF;IAChF,YAAY,EAAE,OAAO,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAgB,SAAS,IAAI,MAAM,CAclC;AAcD,iFAAiF;AACjF,wBAAsB,KAAK,CACzB,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAE,SAAS,EACpB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;;;;GAMG;AACH,qBAAa,kBAAkB;IAGjB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,KAAK,CAAS;gBAEO,OAAO,EAAE,gBAAgB;IAEtD,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,yEAAyE;IACzE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAW1C;AAED,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,GAAE,iBAAsB,GAC9B,MAAM,IAAI,CA0CZ"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Analytics: session, impression, click.
3
+ *
4
+ * Impression ownership (plan v1.1 §3.4) is the load-bearing idea. The SDK
5
+ * renders inside the customer's page, so it is the only party that knows an
6
+ * impression actually happened — the server knows only that bytes were
7
+ * requested, which is not the same thing and is spectacularly not the same
8
+ * thing behind a CDN. So every SDK request carries `sdk=1`, the server
9
+ * suppresses its own impression, and this module owns the count.
10
+ *
11
+ * Which means the rules here are not conveniences, they are the integrity of
12
+ * CVG:
13
+ *
14
+ * - ONE impression per island per page view. Not per render, not per state
15
+ * change, not per React strict-mode double-invoke.
16
+ * - VISIBILITY, not mount. A component rendered below the fold, or in a
17
+ * background tab, has not been seen.
18
+ * - the impression carries what the viewer was actually shown, including
19
+ * whether the trust chrome was on.
20
+ */
21
+ const SESSION_KEY = "st:session";
22
+ /**
23
+ * Stable per-tab session id.
24
+ *
25
+ * sessionStorage, not localStorage: a session is a visit, and a
26
+ * localStorage-backed id would make one visitor's return look like a
27
+ * continuation of a visit that ended days ago. Falls back to a memory-only id
28
+ * where storage is unavailable, which degrades attribution rather than
29
+ * breaking rendering.
30
+ */
31
+ let memorySessionId = null;
32
+ export function sessionId() {
33
+ try {
34
+ if (typeof sessionStorage !== "undefined") {
35
+ const existing = sessionStorage.getItem(SESSION_KEY);
36
+ if (existing)
37
+ return existing;
38
+ const created = randomId();
39
+ sessionStorage.setItem(SESSION_KEY, created);
40
+ return created;
41
+ }
42
+ }
43
+ catch {
44
+ // Storage disabled or partitioned — fall through.
45
+ }
46
+ memorySessionId ??= randomId();
47
+ return memorySessionId;
48
+ }
49
+ function randomId() {
50
+ const globalCrypto = typeof crypto !== "undefined" ? crypto : undefined;
51
+ if (globalCrypto?.randomUUID)
52
+ return globalCrypto.randomUUID();
53
+ if (globalCrypto?.getRandomValues) {
54
+ const bytes = globalCrypto.getRandomValues(new Uint8Array(16));
55
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
56
+ }
57
+ // Last resort. Only reached in environments with no crypto at all, where
58
+ // the id's only job is to group one tab's events with each other.
59
+ return `st-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
60
+ }
61
+ /** POST an event. Fire-and-forget: telemetry never blocks or breaks a render. */
62
+ export async function track(context, eventType, eventData = {}) {
63
+ try {
64
+ // URL building and serialisation live INSIDE the try: a bad apiBase or an
65
+ // unserialisable payload (a BigInt, a cycle) throws synchronously, and
66
+ // callers fire this as `void track(...)` — an escape here is an
67
+ // unhandled rejection in the customer's page.
68
+ const url = new URL("/api/embed/v1/track", context.apiBase).toString();
69
+ const body = JSON.stringify({
70
+ organizationId: context.organizationId,
71
+ campaignId: context.campaignId,
72
+ variantId: context.variantId,
73
+ surfaceId: context.surfaceId,
74
+ eventType,
75
+ eventData: { ...eventData, sessionId: sessionId(), sdk: true },
76
+ });
77
+ // keepalive so an impression recorded as the viewer navigates away still
78
+ // makes it out; without it the last impression of every visit is lost,
79
+ // which biases CVG toward pages people stayed on.
80
+ await fetch(url, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body,
84
+ keepalive: true,
85
+ credentials: "omit",
86
+ });
87
+ }
88
+ catch {
89
+ // Swallowed on purpose. A failed beacon is a lost data point; a thrown
90
+ // beacon inside a render tree is a broken customer page.
91
+ }
92
+ }
93
+ /**
94
+ * Records at most one impression per island per page view.
95
+ *
96
+ * The guard is an instance, not a module global, so two islands on one page
97
+ * each get their own — while a single island being re-rendered, re-subscribed
98
+ * or double-invoked by React strict mode gets exactly one.
99
+ */
100
+ export class ImpressionRecorder {
101
+ context;
102
+ fired = false;
103
+ constructor(context) {
104
+ this.context = context;
105
+ }
106
+ get hasFired() {
107
+ return this.fired;
108
+ }
109
+ /** Idempotent. Returns true only for the call that actually recorded. */
110
+ record(detail) {
111
+ if (this.fired)
112
+ return false;
113
+ this.fired = true;
114
+ void track(this.context, "impression", {
115
+ contentSource: detail.contentSource,
116
+ chromeMarked: detail.chromeMarked,
117
+ ...(detail.degraded ? { degraded: detail.degraded } : {}),
118
+ ...(detail.island ? { island: detail.island } : {}),
119
+ });
120
+ return true;
121
+ }
122
+ }
123
+ /**
124
+ * Fire `onVisible` once, when the element has been meaningfully on screen.
125
+ *
126
+ * Where IntersectionObserver does not exist, this fires immediately rather
127
+ * than never: under-counting every impression in older browsers would be a
128
+ * silent, systematic bias in the numbers a customer is billed and optimised
129
+ * against. Over-counting there is the lesser wrong, and it is at least
130
+ * visible.
131
+ */
132
+ export function observeVisibility(element, onVisible, options = {}) {
133
+ // IntersectionObserver THROWS on a threshold outside [0,1], and this is
134
+ // caller-supplied — clamping turns a render-tree crash into a slightly
135
+ // eager or slightly strict impression.
136
+ const threshold = Math.min(1, Math.max(0, options.threshold ?? 0.5));
137
+ const dwellMs = options.dwellMs ?? 300;
138
+ if (typeof IntersectionObserver === "undefined") {
139
+ onVisible();
140
+ return () => { };
141
+ }
142
+ let dwellTimer = null;
143
+ let done = false;
144
+ const observer = new IntersectionObserver((entries) => {
145
+ for (const entry of entries) {
146
+ if (done)
147
+ return;
148
+ if (entry.isIntersecting && entry.intersectionRatio >= threshold) {
149
+ dwellTimer ??= setTimeout(() => {
150
+ done = true;
151
+ observer.disconnect();
152
+ onVisible();
153
+ }, dwellMs);
154
+ }
155
+ else if (dwellTimer) {
156
+ // Left the viewport before the dwell elapsed: it does not count, and
157
+ // the clock restarts rather than resuming.
158
+ clearTimeout(dwellTimer);
159
+ dwellTimer = null;
160
+ }
161
+ }
162
+ }, { threshold: [0, threshold, 1] });
163
+ observer.observe(element);
164
+ return () => {
165
+ if (dwellTimer)
166
+ clearTimeout(dwellTimer);
167
+ observer.disconnect();
168
+ };
169
+ }
170
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Audio engine — W3, plan v1.1 decision (b): audio is CORE-ONLY.
3
+ *
4
+ * `window.__squidAudio` was an inline IIFE in the shell template. Moving it
5
+ * here makes it versioned and patchable: a provider changes their embed API
6
+ * and one package bump reaches every consumer, instead of every customer
7
+ * needing to re-copy a component.
8
+ *
9
+ * The accepted trade-off is that consumers cannot customise playback
10
+ * behaviour without forking. The mitigation is the shape of this module: what
11
+ * moves into core is the ENGINE — adapters, gesture wiring, the listen clock,
12
+ * media session — and NOT the chrome. `getState()` and `on()` expose
13
+ * everything a copied component needs to render its own player UI against an
14
+ * engine it does not own. Moving the shell's markup in here as well would have
15
+ * been "just moving the file", and would have made every consumer's player
16
+ * look like signage.
17
+ *
18
+ * Two invariants worth stating because both are load-bearing:
19
+ *
20
+ * - AUTOPLAY IS NEVER USED. Tap to play, always. Autoplaying audio in
21
+ * someone else's page is the single fastest way for a customer to remove
22
+ * the SDK.
23
+ * - ONE PLAYER AT A TIME. Mounting a second destroys the first, so a page
24
+ * that cycles variants cannot end up with two tracks over each other.
25
+ */
26
+ export type AudioProvider = "suno" | "spotify";
27
+ export interface AudioBlock {
28
+ provider: AudioProvider;
29
+ /** Direct media URL (suno) or provider URI (spotify). */
30
+ audioUrl?: string;
31
+ trackRef?: string;
32
+ title?: string;
33
+ artist?: string;
34
+ durationMs?: number;
35
+ loop?: boolean;
36
+ artwork?: {
37
+ url: string;
38
+ };
39
+ }
40
+ export type AudioEvent = "play" | "pause" | "progress" | "complete" | "error" | "ready";
41
+ export interface AudioState {
42
+ provider: AudioProvider;
43
+ playing: boolean;
44
+ positionMs: number;
45
+ durationMs: number;
46
+ /** 0..1. Zero when the duration is not yet known — never NaN. */
47
+ percent: number;
48
+ /** Wall-clock time this track was actually audible. Drives listen analytics. */
49
+ listenedMs: number;
50
+ ended: boolean;
51
+ errored: boolean;
52
+ }
53
+ export interface AudioCapabilities {
54
+ /**
55
+ * May the consumer render its own transport UI? False for providers whose
56
+ * embed is a fixed iframe we are not permitted to hide or reskin.
57
+ */
58
+ customChrome: boolean;
59
+ /** Does playback work without the viewer holding a provider account? */
60
+ anonymousPlayback: boolean;
61
+ }
62
+ export interface AudioHandle {
63
+ provider: AudioProvider;
64
+ caps: AudioCapabilities;
65
+ getState(): AudioState;
66
+ play(): void;
67
+ pause(): void;
68
+ toggle(): void;
69
+ /** No-op on providers that do not expose seeking. */
70
+ seek(positionMs: number): void;
71
+ on(event: AudioEvent, listener: (state: AudioState) => void): () => void;
72
+ destroy(): void;
73
+ }
74
+ /** The currently mounted player, if any. */
75
+ export declare function activePlayer(): AudioHandle | null;
76
+ /**
77
+ * Mount a player for `block` inside `host`.
78
+ *
79
+ * Destroys any existing player first — the single-player invariant. A page
80
+ * cycling variants would otherwise stack tracks on top of each other, and the
81
+ * viewer has no way to stop one they can no longer see.
82
+ */
83
+ export declare function mountAudio(block: AudioBlock, host: HTMLElement): AudioHandle | null;
84
+ export declare function destroyActiveAudio(): void;
85
+ //# sourceMappingURL=audio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,aAAa,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3B;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAExF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,IAAI,UAAU,CAAC;IACvB,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,qDAAqD;IACrD,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACzE,OAAO,IAAI,IAAI,CAAC;CACjB;AAkbD,4CAA4C;AAC5C,wBAAgB,YAAY,IAAI,WAAW,GAAG,IAAI,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,GAAG,WAAW,GAAG,IAAI,CAYnF;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAIzC"}