@sable-ai/sdk-core 0.1.3 → 0.1.4

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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @sable-ai/sdk-core — loader stub (IIFE entry for the CDN bundle).
3
+ *
4
+ * This file compiles to `dist/sable.iife.js` (~1 KB gzipped). It is the
5
+ * ONLY script a customer loads on page render; the full SDK (~150 KB
6
+ * gzipped including livekit-client) is lazy-loaded as `sable-core.mjs` on
7
+ * the first call to `Sable.start()`. Pages that never open a session pay
8
+ * just the loader cost.
9
+ *
10
+ * Architecture (Stripe.js pattern):
11
+ *
12
+ * Page load: <script src="sdk.withsable.com/v1/sable.js"> → loader (1 KB)
13
+ * User click: Sable.start() → dynamic import of sable-core.mjs
14
+ * Steady state: 1 KB for pages that never start a session
15
+ *
16
+ * The loader pins itself to the same path as sable-core.mjs: both are
17
+ * served from `/v<version>/`, and the loader derives the core URL from
18
+ * its own script src (`document.currentScript.src`). A customer loading
19
+ * `/v0.1.4/sable.js` always pulls `/v0.1.4/sable-core.mjs`; a customer
20
+ * loading `/v1/sable.js` gets `/v1/sable-core.mjs`. Version cohesion is
21
+ * automatic — customers never touch the core URL.
22
+ */
23
+ export {};
@@ -23,7 +23,7 @@ import type { SableAPI, SableEventHandler, SableEvents, StartOptions } from "../
23
23
  * state ownership explicit and the teardown path easier to reason about.
24
24
  */
25
25
  export declare class Session implements SableAPI {
26
- readonly version = "0.1.3";
26
+ readonly version = "0.1.4";
27
27
  private readonly emitter;
28
28
  private activeRoom;
29
29
  private visionHandle;
@@ -4,4 +4,4 @@
4
4
  * Kept in a standalone file so build tooling (GitHub Actions release workflow)
5
5
  * can replace it at publish time without touching anything else.
6
6
  */
7
- export declare const VERSION = "0.1.3";
7
+ export declare const VERSION = "0.1.4";
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@sable-ai/sdk-core",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Sable SDK core — headless runtime (voice + vision + agent RPC) that runs in the user's browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": [
8
8
  "./dist/sable.iife.js",
9
+ "./dist/sable-core.mjs",
9
10
  "./dist/esm/index.js"
10
11
  ],
11
12
  "main": "./dist/esm/index.js",
@@ -33,8 +34,9 @@
33
34
  },
34
35
  "scripts": {
35
36
  "clean": "rm -rf dist",
36
- "build": "bun run clean && bun run build:iife && bun run build:esm && bun run build:types",
37
- "build:iife": "bun build src/index.ts --outfile dist/sable.iife.js --format=iife --minify",
37
+ "build": "bun run clean && bun run build:loader && bun run build:core && bun run build:esm && bun run build:types",
38
+ "build:loader": "bun build src/loader.ts --outfile dist/sable.iife.js --format=iife --minify --target=browser",
39
+ "build:core": "bun build src/index.ts --outfile dist/sable-core.mjs --format=esm --minify --target=browser",
38
40
  "build:esm": "bun build src/index.ts --outdir dist/esm --format=esm --target=browser --external livekit-client",
39
41
  "build:types": "tsc -p tsconfig.build.json",
40
42
  "typecheck": "tsc --noEmit",
package/src/index.test.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { test, expect } from "bun:test";
2
2
  import { VERSION } from "./index";
3
3
 
4
- test("VERSION is exported as 0.1.3", () => {
5
- expect(VERSION).toBe("0.1.3");
4
+ test("VERSION is exported as 0.1.4", () => {
5
+ expect(VERSION).toBe("0.1.4");
6
6
  });
package/src/loader.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @sable-ai/sdk-core — loader stub (IIFE entry for the CDN bundle).
3
+ *
4
+ * This file compiles to `dist/sable.iife.js` (~1 KB gzipped). It is the
5
+ * ONLY script a customer loads on page render; the full SDK (~150 KB
6
+ * gzipped including livekit-client) is lazy-loaded as `sable-core.mjs` on
7
+ * the first call to `Sable.start()`. Pages that never open a session pay
8
+ * just the loader cost.
9
+ *
10
+ * Architecture (Stripe.js pattern):
11
+ *
12
+ * Page load: <script src="sdk.withsable.com/v1/sable.js"> → loader (1 KB)
13
+ * User click: Sable.start() → dynamic import of sable-core.mjs
14
+ * Steady state: 1 KB for pages that never start a session
15
+ *
16
+ * The loader pins itself to the same path as sable-core.mjs: both are
17
+ * served from `/v<version>/`, and the loader derives the core URL from
18
+ * its own script src (`document.currentScript.src`). A customer loading
19
+ * `/v0.1.4/sable.js` always pulls `/v0.1.4/sable-core.mjs`; a customer
20
+ * loading `/v1/sable.js` gets `/v1/sable-core.mjs`. Version cohesion is
21
+ * automatic — customers never touch the core URL.
22
+ */
23
+
24
+ import { VERSION } from "./version";
25
+ import type {
26
+ SableAPI,
27
+ SableEvents,
28
+ SableEventHandler,
29
+ StartOptions,
30
+ } from "./types";
31
+
32
+ // ── Script URL capture ────────────────────────────────────────────────────
33
+ //
34
+ // `document.currentScript` is only defined during SYNCHRONOUS script
35
+ // evaluation. We MUST read it at IIFE top-level, before any async boundary,
36
+ // or it will be null by the time start() is called.
37
+
38
+ const scriptSrc: string =
39
+ typeof document !== "undefined" &&
40
+ document.currentScript instanceof HTMLScriptElement
41
+ ? document.currentScript.src
42
+ : "";
43
+
44
+ // ── Pre-load subscription buffer ──────────────────────────────────────────
45
+ //
46
+ // Customers may call `Sable.on(...)` before `Sable.start(...)` — e.g. to
47
+ // wire up UI observers on page load. We record those subscriptions here
48
+ // and forward them to the real session once the core loads. Each entry
49
+ // tracks a `cancelled` flag + the real unsub function so the loader's
50
+ // returned unsub can reach through to the real subscription after load.
51
+
52
+ interface PendingSub {
53
+ event: keyof SableEvents;
54
+ handler: SableEventHandler<keyof SableEvents>;
55
+ realUnsub?: () => void;
56
+ cancelled?: boolean;
57
+ }
58
+ const pendingSubs: PendingSub[] = [];
59
+
60
+ // ── Core loader (memoised) ────────────────────────────────────────────────
61
+
62
+ let realSable: SableAPI | null = null;
63
+ let corePromise: Promise<SableAPI> | null = null;
64
+
65
+ function loadCore(): Promise<SableAPI> {
66
+ if (realSable) return Promise.resolve(realSable);
67
+ if (corePromise) return corePromise;
68
+
69
+ if (!scriptSrc) {
70
+ return Promise.reject(
71
+ new Error(
72
+ "[Sable] cannot locate SDK script URL. The loader must be loaded " +
73
+ 'via a <script src="..."> tag (document.currentScript was null).',
74
+ ),
75
+ );
76
+ }
77
+
78
+ const coreUrl = new URL("./sable-core.mjs", scriptSrc).href;
79
+ corePromise = (async () => {
80
+ const mod = (await import(/* @vite-ignore */ coreUrl)) as {
81
+ default: SableAPI;
82
+ };
83
+ realSable = mod.default;
84
+
85
+ // Flush queued subscriptions.
86
+ for (const sub of pendingSubs) {
87
+ if (sub.cancelled) continue;
88
+ sub.realUnsub = realSable.on(sub.event, sub.handler);
89
+ }
90
+
91
+ return realSable;
92
+ })();
93
+ return corePromise;
94
+ }
95
+
96
+ // ── Public API proxy ──────────────────────────────────────────────────────
97
+
98
+ const Sable: SableAPI = {
99
+ version: VERSION,
100
+
101
+ async start(opts: StartOptions): Promise<void> {
102
+ const s = await loadCore();
103
+ return s.start(opts);
104
+ },
105
+
106
+ async stop(): Promise<void> {
107
+ // If the user calls stop() before start() resolved, wait for the core
108
+ // to finish loading so we can forward the call — otherwise we'd leave
109
+ // the in-flight start() orphaned.
110
+ if (corePromise && !realSable) await corePromise;
111
+ if (!realSable) return;
112
+ return realSable.stop();
113
+ },
114
+
115
+ on<E extends keyof SableEvents>(
116
+ event: E,
117
+ handler: SableEventHandler<E>,
118
+ ): () => void {
119
+ if (realSable) {
120
+ return realSable.on(event, handler);
121
+ }
122
+ const sub: PendingSub = {
123
+ event,
124
+ handler: handler as SableEventHandler<keyof SableEvents>,
125
+ };
126
+ pendingSubs.push(sub);
127
+ return () => {
128
+ sub.cancelled = true;
129
+ if (sub.realUnsub) sub.realUnsub();
130
+ };
131
+ },
132
+ };
133
+
134
+ // ── Install on window ─────────────────────────────────────────────────────
135
+ //
136
+ // First-write-wins — matches `src/global.ts` semantics for the npm path.
137
+ // If the customer already has a Sable (e.g. mixed npm + script tag on the
138
+ // same page), we don't swap it out.
139
+
140
+ if (typeof window !== "undefined" && !window.Sable) {
141
+ window.Sable = Sable;
142
+ console.log("[Sable] loader ready", VERSION);
143
+ }
package/src/version.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * can replace it at publish time without touching anything else.
6
6
  */
7
7
 
8
- export const VERSION = "0.1.3";
8
+ export const VERSION = "0.1.4";