@layla-network/sdk 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ChatCompletionStream: () => ChatCompletionStream,
24
+ Layla: () => LaylaSDK,
25
+ LaylaAbortError: () => LaylaAbortError,
26
+ LaylaBridgeUnavailableError: () => LaylaBridgeUnavailableError,
27
+ LaylaError: () => LaylaError,
28
+ LaylaSDK: () => LaylaSDK,
29
+ default: () => index_default
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+ var LaylaError = class extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = "LaylaError";
36
+ }
37
+ };
38
+ var LaylaAbortError = class extends LaylaError {
39
+ constructor(message = "Request was aborted") {
40
+ super(message);
41
+ this.name = "LaylaAbortError";
42
+ }
43
+ };
44
+ var LaylaBridgeUnavailableError = class extends LaylaError {
45
+ constructor() {
46
+ super(
47
+ "Layla bridge unavailable: window.ReactNativeWebView is not present. Make sure this code runs inside the Layla WebView and that the <WebView> has its `onMessage` prop set (that is what injects the bridge)."
48
+ );
49
+ this.name = "LaylaBridgeUnavailableError";
50
+ }
51
+ };
52
+ var Deferred = class {
53
+ constructor() {
54
+ this.promise = new Promise((res, rej) => {
55
+ this.resolve = res;
56
+ this.reject = rej;
57
+ });
58
+ }
59
+ };
60
+ var _LaylaBridge = class _LaylaBridge {
61
+ constructor() {
62
+ this.queue = [];
63
+ this.active = null;
64
+ this.listening = false;
65
+ this.onWindowMessage = (event) => {
66
+ const raw = event.data;
67
+ if (typeof raw !== "string") return;
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(raw);
71
+ } catch {
72
+ return;
73
+ }
74
+ if (!parsed || typeof parsed.event !== "string") return;
75
+ const job = this.active;
76
+ if (!job) return;
77
+ const sink = job.sink;
78
+ switch (parsed.event) {
79
+ case "on_message": {
80
+ const data = parsed.data ?? { msg: "", delta: "" };
81
+ sink.handleDelta(data.delta ?? "", data.msg ?? "");
82
+ break;
83
+ }
84
+ case "on_message_end": {
85
+ sink.handleEnd();
86
+ this.finishActive();
87
+ break;
88
+ }
89
+ case "on_error": {
90
+ const data = parsed.data;
91
+ sink.handleError(new LaylaError(data?.message || "Layla model error"));
92
+ this.finishActive();
93
+ break;
94
+ }
95
+ default:
96
+ break;
97
+ }
98
+ };
99
+ }
100
+ static shared() {
101
+ if (!_LaylaBridge.instance) _LaylaBridge.instance = new _LaylaBridge();
102
+ return _LaylaBridge.instance;
103
+ }
104
+ ensureListening() {
105
+ if (this.listening || typeof window === "undefined") return;
106
+ window.addEventListener("message", this.onWindowMessage);
107
+ this.listening = true;
108
+ }
109
+ enqueue(message, sink) {
110
+ this.ensureListening();
111
+ this.queue.push({ message, sink });
112
+ this.pump();
113
+ }
114
+ /** Remove a not-yet-started job (used when a queued request is aborted). */
115
+ cancelQueued(sink) {
116
+ this.queue = this.queue.filter((job) => job.sink !== sink);
117
+ }
118
+ finishActive() {
119
+ this.active = null;
120
+ this.pump();
121
+ }
122
+ pump() {
123
+ if (this.active) return;
124
+ const next = this.queue.shift();
125
+ if (!next) return;
126
+ if (next.sink.isClosed()) {
127
+ this.pump();
128
+ return;
129
+ }
130
+ this.active = next;
131
+ this.send(next);
132
+ }
133
+ send(job) {
134
+ if (typeof window === "undefined" || !window.ReactNativeWebView) {
135
+ this.active = null;
136
+ job.sink.handleError(new LaylaBridgeUnavailableError());
137
+ this.pump();
138
+ return;
139
+ }
140
+ window.ReactNativeWebView.postMessage(JSON.stringify(job.message));
141
+ }
142
+ };
143
+ _LaylaBridge.instance = null;
144
+ var LaylaBridge = _LaylaBridge;
145
+ var ChatCompletionStream = class {
146
+ constructor(model) {
147
+ this.id = `chatcmpl-layla-${Date.now()}-${Math.floor(
148
+ Math.random() * 1e6
149
+ )}`;
150
+ this.created = Math.floor(Date.now() / 1e3);
151
+ this.buffer = [];
152
+ this.resolvers = [];
153
+ this.rejectors = [];
154
+ this.snapshot = "";
155
+ this.ended = false;
156
+ this.closed = false;
157
+ this.failure = null;
158
+ this.listeners = {};
159
+ this.finalDeferred = new Deferred();
160
+ this.model = model;
161
+ this.finalDeferred.promise.catch(() => void 0);
162
+ }
163
+ on(event, listener) {
164
+ var _a;
165
+ ((_a = this.listeners)[event] || (_a[event] = [])).push(listener);
166
+ return this;
167
+ }
168
+ off(event, listener) {
169
+ const ls = this.listeners[event];
170
+ if (ls) this.listeners[event] = ls.filter((l) => l !== listener);
171
+ return this;
172
+ }
173
+ emit(event, ...args) {
174
+ const ls = this.listeners[event];
175
+ if (!ls) return;
176
+ for (const l of ls.slice()) {
177
+ try {
178
+ l(...args);
179
+ } catch {
180
+ }
181
+ }
182
+ }
183
+ /* ---- StreamSink: driven by the bridge --------------------------------- */
184
+ handleDelta(delta, snapshot) {
185
+ if (this.closed) return;
186
+ this.snapshot = snapshot || this.snapshot + delta;
187
+ const chunk = this.makeChunk({ content: delta }, null);
188
+ this.pushChunk(chunk);
189
+ this.emit("chunk", chunk);
190
+ if (delta) this.emit("content", delta, this.snapshot);
191
+ }
192
+ handleEnd() {
193
+ if (this.closed) return;
194
+ const finalChunk = this.makeChunk({}, "stop");
195
+ this.pushChunk(finalChunk);
196
+ this.emit("chunk", finalChunk);
197
+ this.ended = true;
198
+ this.closed = true;
199
+ this.drainDone();
200
+ const completion = this.buildCompletion();
201
+ this.emit("end");
202
+ this.finalDeferred.resolve(completion);
203
+ }
204
+ handleError(err) {
205
+ if (this.closed) return;
206
+ this.failure = err;
207
+ this.closed = true;
208
+ this.drainError(err);
209
+ this.emit("error", err);
210
+ this.finalDeferred.reject(err);
211
+ }
212
+ isClosed() {
213
+ return this.closed;
214
+ }
215
+ /** Abort from the consumer side. */
216
+ abort(reason) {
217
+ if (this.closed) return;
218
+ const err = reason instanceof Error ? reason : new LaylaAbortError();
219
+ LaylaBridge.shared().cancelQueued(this);
220
+ this.handleError(err);
221
+ }
222
+ /* ---- async iteration -------------------------------------------------- */
223
+ next() {
224
+ if (this.buffer.length) {
225
+ return Promise.resolve({ value: this.buffer.shift(), done: false });
226
+ }
227
+ if (this.failure) return Promise.reject(this.failure);
228
+ if (this.ended) {
229
+ return Promise.resolve({ value: void 0, done: true });
230
+ }
231
+ return new Promise((resolve, reject) => {
232
+ this.resolvers.push(resolve);
233
+ this.rejectors.push(reject);
234
+ });
235
+ }
236
+ /** Breaking out of `for await` aborts the request, like the OpenAI SDK. */
237
+ return() {
238
+ this.abort(new LaylaAbortError("Stream consumer stopped"));
239
+ return Promise.resolve({ value: void 0, done: true });
240
+ }
241
+ [Symbol.asyncIterator]() {
242
+ return this;
243
+ }
244
+ /* ---- convenience promises -------------------------------------------- */
245
+ finalChatCompletion() {
246
+ return this.finalDeferred.promise;
247
+ }
248
+ async finalContent() {
249
+ const completion = await this.finalDeferred.promise;
250
+ return completion.choices[0]?.message.content ?? "";
251
+ }
252
+ /* ---- internals -------------------------------------------------------- */
253
+ pushChunk(chunk) {
254
+ if (this.resolvers.length) {
255
+ this.resolvers.shift()({ value: chunk, done: false });
256
+ this.rejectors.shift();
257
+ } else {
258
+ this.buffer.push(chunk);
259
+ }
260
+ }
261
+ drainDone() {
262
+ while (this.resolvers.length) {
263
+ this.resolvers.shift()({ value: void 0, done: true });
264
+ this.rejectors.shift();
265
+ }
266
+ }
267
+ drainError(err) {
268
+ while (this.rejectors.length) {
269
+ this.rejectors.shift()(err);
270
+ this.resolvers.shift();
271
+ }
272
+ }
273
+ makeChunk(delta, finish) {
274
+ return {
275
+ id: this.id,
276
+ object: "chat.completion.chunk",
277
+ created: this.created,
278
+ model: this.model,
279
+ choices: [{ index: 0, delta, finish_reason: finish }]
280
+ };
281
+ }
282
+ buildCompletion() {
283
+ return {
284
+ id: this.id,
285
+ object: "chat.completion",
286
+ created: this.created,
287
+ model: this.model,
288
+ choices: [
289
+ {
290
+ index: 0,
291
+ message: { role: "assistant", content: this.snapshot },
292
+ finish_reason: "stop"
293
+ }
294
+ ]
295
+ };
296
+ }
297
+ };
298
+ var Completions = class {
299
+ create(body) {
300
+ const stream = this.startStream(
301
+ body.messages,
302
+ body.model ?? "layla",
303
+ body.signal
304
+ );
305
+ if (body.stream) return Promise.resolve(stream);
306
+ return stream.finalChatCompletion();
307
+ }
308
+ /**
309
+ * OpenAI-style helper: returns the live stream object synchronously (not a
310
+ * promise), so you can attach `.on(...)` listeners before any token arrives.
311
+ */
312
+ stream(body) {
313
+ return this.startStream(body.messages, body.model ?? "layla", body.signal);
314
+ }
315
+ startStream(messages, model, signal) {
316
+ const stream = new ChatCompletionStream(model);
317
+ if (signal?.aborted) {
318
+ queueMicrotask(() => stream.abort(new LaylaAbortError()));
319
+ return stream;
320
+ }
321
+ if (signal) {
322
+ signal.addEventListener(
323
+ "abort",
324
+ () => stream.abort(new LaylaAbortError()),
325
+ { once: true }
326
+ );
327
+ }
328
+ LaylaBridge.shared().enqueue({ cmd: "send_message", data: messages }, stream);
329
+ return stream;
330
+ }
331
+ };
332
+ var Chat = class {
333
+ constructor() {
334
+ this.completions = new Completions();
335
+ }
336
+ };
337
+ var LaylaSDK = class {
338
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
339
+ constructor(_options = {}) {
340
+ this.chat = new Chat();
341
+ }
342
+ };
343
+ var index_default = LaylaSDK;
344
+ // Annotate the CommonJS export names for ESM import in node:
345
+ 0 && (module.exports = {
346
+ ChatCompletionStream,
347
+ Layla,
348
+ LaylaAbortError,
349
+ LaylaBridgeUnavailableError,
350
+ LaylaError,
351
+ LaylaSDK
352
+ });
@@ -0,0 +1,208 @@
1
+ /**
2
+ * LaylaSDK
3
+ * --------
4
+ * A web-side client (runs INSIDE the Layla WebView) that talks to the native
5
+ * Layla app over the React Native WebView bridge, exposed through an API that
6
+ * mirrors the OpenAI JavaScript SDK so existing OpenAI code ports with minimal
7
+ * friction.
8
+ *
9
+ * The native host runs the model and streams tokens back; this SDK turns that
10
+ * event stream into the OpenAI shapes (`ChatCompletion`, `ChatCompletionChunk`)
11
+ * and an async-iterable stream.
12
+ *
13
+ * Wire protocol (must match the React Native host):
14
+ * Web -> RN : { cmd: 'send_message', data: LaylaChatMessage[] }
15
+ * RN -> Web : { event: 'on_message', data: { msg, delta } } (per token)
16
+ * { event: 'on_message_end' } (end)
17
+ * { event: 'on_error', data: { message } } (error)
18
+ *
19
+ * Quick start:
20
+ *
21
+ * import { LaylaSDK } from './layla-sdk';
22
+ * const layla = new LaylaSDK();
23
+ *
24
+ * // Streaming (async iteration) -- the OpenAI way:
25
+ * const stream = await layla.chat.completions.create({
26
+ * messages: [{ role: 'user', content: 'Hello' }],
27
+ * stream: true,
28
+ * });
29
+ * for await (const chunk of stream) {
30
+ * process_token(chunk.choices[0]?.delta?.content ?? '');
31
+ * }
32
+ *
33
+ * // Streaming (event helper) -- maps cleanly onto Layla's delta/snapshot:
34
+ * const s = layla.chat.completions.stream({
35
+ * messages: [{ role: 'user', content: 'Hello' }],
36
+ * });
37
+ * s.on('content', (delta, snapshot) => render(snapshot));
38
+ * const final = await s.finalContent();
39
+ *
40
+ * // Non-streaming -- resolves once when the model is done:
41
+ * const completion = await layla.chat.completions.create({
42
+ * messages: [{ role: 'user', content: 'Hello' }],
43
+ * });
44
+ * console.log(completion.choices[0].message.content);
45
+ */
46
+ type LaylaChatRole = 'system' | 'user' | 'assistant' | 'tool' | 'function';
47
+ /** An OpenAI-style chat message. */
48
+ interface LaylaChatMessage {
49
+ role: LaylaChatRole;
50
+ content: string | null;
51
+ name?: string;
52
+ }
53
+ /** Web -> RN command. */
54
+ interface LaylaApiMessage {
55
+ cmd: 'send_message';
56
+ data: LaylaChatMessage[];
57
+ }
58
+ /** RN -> Web: a streamed token. `msg` is the full snapshot, `delta` is new. */
59
+ interface LaylaApiEvent_onMsg {
60
+ event: 'on_message';
61
+ data: {
62
+ msg: string;
63
+ delta: string;
64
+ };
65
+ }
66
+ /** RN -> Web: stream finished. */
67
+ interface LaylaApiEvent_onMsgEnd {
68
+ event: 'on_message_end';
69
+ }
70
+ /** RN -> Web: error. */
71
+ interface LaylaApiEvent_onError {
72
+ event: 'on_error';
73
+ data: {
74
+ message: string;
75
+ };
76
+ }
77
+ type LaylaApiEvent = LaylaApiEvent_onMsg | LaylaApiEvent_onMsgEnd | LaylaApiEvent_onError;
78
+ interface ChatCompletionChunk {
79
+ id: string;
80
+ object: 'chat.completion.chunk';
81
+ created: number;
82
+ model: string;
83
+ choices: Array<{
84
+ index: number;
85
+ delta: {
86
+ role?: 'assistant';
87
+ content?: string;
88
+ };
89
+ finish_reason: 'stop' | null;
90
+ }>;
91
+ }
92
+ interface ChatCompletion {
93
+ id: string;
94
+ object: 'chat.completion';
95
+ created: number;
96
+ model: string;
97
+ choices: Array<{
98
+ index: number;
99
+ message: {
100
+ role: 'assistant';
101
+ content: string;
102
+ };
103
+ finish_reason: 'stop';
104
+ }>;
105
+ }
106
+ interface ChatCompletionCreateParamsBase {
107
+ messages: LaylaChatMessage[];
108
+ /**
109
+ * Accepted for OpenAI compatibility. The Layla host currently picks the
110
+ * model itself, so this is only used to populate the `model` field on the
111
+ * returned objects unless you extend the `send_message` protocol.
112
+ */
113
+ model?: string;
114
+ stream?: boolean;
115
+ /** Abort the request from the consumer side. */
116
+ signal?: AbortSignal;
117
+ }
118
+ interface ChatCompletionCreateParamsNonStreaming extends ChatCompletionCreateParamsBase {
119
+ stream?: false;
120
+ }
121
+ interface ChatCompletionCreateParamsStreaming extends ChatCompletionCreateParamsBase {
122
+ stream: true;
123
+ }
124
+ declare class LaylaError extends Error {
125
+ constructor(message: string);
126
+ }
127
+ declare class LaylaAbortError extends LaylaError {
128
+ constructor(message?: string);
129
+ }
130
+ declare class LaylaBridgeUnavailableError extends LaylaError {
131
+ constructor();
132
+ }
133
+ declare global {
134
+ interface Window {
135
+ ReactNativeWebView?: {
136
+ postMessage: (message: string) => void;
137
+ };
138
+ }
139
+ }
140
+ /** Implemented by ChatCompletionStream; the bridge drives it. */
141
+ interface StreamSink {
142
+ handleDelta(delta: string, snapshot: string): void;
143
+ handleEnd(): void;
144
+ handleError(err: Error): void;
145
+ isClosed(): boolean;
146
+ }
147
+ type Listener = (...args: any[]) => void;
148
+ declare class ChatCompletionStream implements StreamSink, AsyncIterable<ChatCompletionChunk> {
149
+ private readonly id;
150
+ private readonly created;
151
+ private readonly model;
152
+ private buffer;
153
+ private resolvers;
154
+ private rejectors;
155
+ private snapshot;
156
+ private ended;
157
+ private closed;
158
+ private failure;
159
+ private listeners;
160
+ private finalDeferred;
161
+ constructor(model: string);
162
+ on(event: 'content', listener: (delta: string, snapshot: string) => void): this;
163
+ on(event: 'chunk', listener: (chunk: ChatCompletionChunk) => void): this;
164
+ on(event: 'end', listener: () => void): this;
165
+ on(event: 'error', listener: (err: Error) => void): this;
166
+ off(event: string, listener: Listener): this;
167
+ private emit;
168
+ handleDelta(delta: string, snapshot: string): void;
169
+ handleEnd(): void;
170
+ handleError(err: Error): void;
171
+ isClosed(): boolean;
172
+ /** Abort from the consumer side. */
173
+ abort(reason?: unknown): void;
174
+ next(): Promise<IteratorResult<ChatCompletionChunk>>;
175
+ /** Breaking out of `for await` aborts the request, like the OpenAI SDK. */
176
+ return(): Promise<IteratorResult<ChatCompletionChunk>>;
177
+ [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk>;
178
+ finalChatCompletion(): Promise<ChatCompletion>;
179
+ finalContent(): Promise<string>;
180
+ private pushChunk;
181
+ private drainDone;
182
+ private drainError;
183
+ private makeChunk;
184
+ private buildCompletion;
185
+ }
186
+ declare class Completions {
187
+ create(body: ChatCompletionCreateParamsNonStreaming): Promise<ChatCompletion>;
188
+ create(body: ChatCompletionCreateParamsStreaming): Promise<ChatCompletionStream>;
189
+ /**
190
+ * OpenAI-style helper: returns the live stream object synchronously (not a
191
+ * promise), so you can attach `.on(...)` listeners before any token arrives.
192
+ */
193
+ stream(body: Omit<ChatCompletionCreateParamsBase, 'stream'>): ChatCompletionStream;
194
+ private startStream;
195
+ }
196
+ declare class Chat {
197
+ readonly completions: Completions;
198
+ }
199
+ interface LaylaSDKOptions {
200
+ /** Reserved for future use (e.g. default model). */
201
+ model?: string;
202
+ }
203
+ declare class LaylaSDK {
204
+ readonly chat: Chat;
205
+ constructor(_options?: LaylaSDKOptions);
206
+ }
207
+
208
+ export { type ChatCompletion, type ChatCompletionChunk, type ChatCompletionCreateParamsBase, type ChatCompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming, ChatCompletionStream, LaylaSDK as Layla, LaylaAbortError, type LaylaApiEvent, type LaylaApiEvent_onError, type LaylaApiEvent_onMsg, type LaylaApiEvent_onMsgEnd, type LaylaApiMessage, LaylaBridgeUnavailableError, type LaylaChatMessage, type LaylaChatRole, LaylaError, LaylaSDK, type LaylaSDKOptions, LaylaSDK as default };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * LaylaSDK
3
+ * --------
4
+ * A web-side client (runs INSIDE the Layla WebView) that talks to the native
5
+ * Layla app over the React Native WebView bridge, exposed through an API that
6
+ * mirrors the OpenAI JavaScript SDK so existing OpenAI code ports with minimal
7
+ * friction.
8
+ *
9
+ * The native host runs the model and streams tokens back; this SDK turns that
10
+ * event stream into the OpenAI shapes (`ChatCompletion`, `ChatCompletionChunk`)
11
+ * and an async-iterable stream.
12
+ *
13
+ * Wire protocol (must match the React Native host):
14
+ * Web -> RN : { cmd: 'send_message', data: LaylaChatMessage[] }
15
+ * RN -> Web : { event: 'on_message', data: { msg, delta } } (per token)
16
+ * { event: 'on_message_end' } (end)
17
+ * { event: 'on_error', data: { message } } (error)
18
+ *
19
+ * Quick start:
20
+ *
21
+ * import { LaylaSDK } from './layla-sdk';
22
+ * const layla = new LaylaSDK();
23
+ *
24
+ * // Streaming (async iteration) -- the OpenAI way:
25
+ * const stream = await layla.chat.completions.create({
26
+ * messages: [{ role: 'user', content: 'Hello' }],
27
+ * stream: true,
28
+ * });
29
+ * for await (const chunk of stream) {
30
+ * process_token(chunk.choices[0]?.delta?.content ?? '');
31
+ * }
32
+ *
33
+ * // Streaming (event helper) -- maps cleanly onto Layla's delta/snapshot:
34
+ * const s = layla.chat.completions.stream({
35
+ * messages: [{ role: 'user', content: 'Hello' }],
36
+ * });
37
+ * s.on('content', (delta, snapshot) => render(snapshot));
38
+ * const final = await s.finalContent();
39
+ *
40
+ * // Non-streaming -- resolves once when the model is done:
41
+ * const completion = await layla.chat.completions.create({
42
+ * messages: [{ role: 'user', content: 'Hello' }],
43
+ * });
44
+ * console.log(completion.choices[0].message.content);
45
+ */
46
+ type LaylaChatRole = 'system' | 'user' | 'assistant' | 'tool' | 'function';
47
+ /** An OpenAI-style chat message. */
48
+ interface LaylaChatMessage {
49
+ role: LaylaChatRole;
50
+ content: string | null;
51
+ name?: string;
52
+ }
53
+ /** Web -> RN command. */
54
+ interface LaylaApiMessage {
55
+ cmd: 'send_message';
56
+ data: LaylaChatMessage[];
57
+ }
58
+ /** RN -> Web: a streamed token. `msg` is the full snapshot, `delta` is new. */
59
+ interface LaylaApiEvent_onMsg {
60
+ event: 'on_message';
61
+ data: {
62
+ msg: string;
63
+ delta: string;
64
+ };
65
+ }
66
+ /** RN -> Web: stream finished. */
67
+ interface LaylaApiEvent_onMsgEnd {
68
+ event: 'on_message_end';
69
+ }
70
+ /** RN -> Web: error. */
71
+ interface LaylaApiEvent_onError {
72
+ event: 'on_error';
73
+ data: {
74
+ message: string;
75
+ };
76
+ }
77
+ type LaylaApiEvent = LaylaApiEvent_onMsg | LaylaApiEvent_onMsgEnd | LaylaApiEvent_onError;
78
+ interface ChatCompletionChunk {
79
+ id: string;
80
+ object: 'chat.completion.chunk';
81
+ created: number;
82
+ model: string;
83
+ choices: Array<{
84
+ index: number;
85
+ delta: {
86
+ role?: 'assistant';
87
+ content?: string;
88
+ };
89
+ finish_reason: 'stop' | null;
90
+ }>;
91
+ }
92
+ interface ChatCompletion {
93
+ id: string;
94
+ object: 'chat.completion';
95
+ created: number;
96
+ model: string;
97
+ choices: Array<{
98
+ index: number;
99
+ message: {
100
+ role: 'assistant';
101
+ content: string;
102
+ };
103
+ finish_reason: 'stop';
104
+ }>;
105
+ }
106
+ interface ChatCompletionCreateParamsBase {
107
+ messages: LaylaChatMessage[];
108
+ /**
109
+ * Accepted for OpenAI compatibility. The Layla host currently picks the
110
+ * model itself, so this is only used to populate the `model` field on the
111
+ * returned objects unless you extend the `send_message` protocol.
112
+ */
113
+ model?: string;
114
+ stream?: boolean;
115
+ /** Abort the request from the consumer side. */
116
+ signal?: AbortSignal;
117
+ }
118
+ interface ChatCompletionCreateParamsNonStreaming extends ChatCompletionCreateParamsBase {
119
+ stream?: false;
120
+ }
121
+ interface ChatCompletionCreateParamsStreaming extends ChatCompletionCreateParamsBase {
122
+ stream: true;
123
+ }
124
+ declare class LaylaError extends Error {
125
+ constructor(message: string);
126
+ }
127
+ declare class LaylaAbortError extends LaylaError {
128
+ constructor(message?: string);
129
+ }
130
+ declare class LaylaBridgeUnavailableError extends LaylaError {
131
+ constructor();
132
+ }
133
+ declare global {
134
+ interface Window {
135
+ ReactNativeWebView?: {
136
+ postMessage: (message: string) => void;
137
+ };
138
+ }
139
+ }
140
+ /** Implemented by ChatCompletionStream; the bridge drives it. */
141
+ interface StreamSink {
142
+ handleDelta(delta: string, snapshot: string): void;
143
+ handleEnd(): void;
144
+ handleError(err: Error): void;
145
+ isClosed(): boolean;
146
+ }
147
+ type Listener = (...args: any[]) => void;
148
+ declare class ChatCompletionStream implements StreamSink, AsyncIterable<ChatCompletionChunk> {
149
+ private readonly id;
150
+ private readonly created;
151
+ private readonly model;
152
+ private buffer;
153
+ private resolvers;
154
+ private rejectors;
155
+ private snapshot;
156
+ private ended;
157
+ private closed;
158
+ private failure;
159
+ private listeners;
160
+ private finalDeferred;
161
+ constructor(model: string);
162
+ on(event: 'content', listener: (delta: string, snapshot: string) => void): this;
163
+ on(event: 'chunk', listener: (chunk: ChatCompletionChunk) => void): this;
164
+ on(event: 'end', listener: () => void): this;
165
+ on(event: 'error', listener: (err: Error) => void): this;
166
+ off(event: string, listener: Listener): this;
167
+ private emit;
168
+ handleDelta(delta: string, snapshot: string): void;
169
+ handleEnd(): void;
170
+ handleError(err: Error): void;
171
+ isClosed(): boolean;
172
+ /** Abort from the consumer side. */
173
+ abort(reason?: unknown): void;
174
+ next(): Promise<IteratorResult<ChatCompletionChunk>>;
175
+ /** Breaking out of `for await` aborts the request, like the OpenAI SDK. */
176
+ return(): Promise<IteratorResult<ChatCompletionChunk>>;
177
+ [Symbol.asyncIterator](): AsyncIterator<ChatCompletionChunk>;
178
+ finalChatCompletion(): Promise<ChatCompletion>;
179
+ finalContent(): Promise<string>;
180
+ private pushChunk;
181
+ private drainDone;
182
+ private drainError;
183
+ private makeChunk;
184
+ private buildCompletion;
185
+ }
186
+ declare class Completions {
187
+ create(body: ChatCompletionCreateParamsNonStreaming): Promise<ChatCompletion>;
188
+ create(body: ChatCompletionCreateParamsStreaming): Promise<ChatCompletionStream>;
189
+ /**
190
+ * OpenAI-style helper: returns the live stream object synchronously (not a
191
+ * promise), so you can attach `.on(...)` listeners before any token arrives.
192
+ */
193
+ stream(body: Omit<ChatCompletionCreateParamsBase, 'stream'>): ChatCompletionStream;
194
+ private startStream;
195
+ }
196
+ declare class Chat {
197
+ readonly completions: Completions;
198
+ }
199
+ interface LaylaSDKOptions {
200
+ /** Reserved for future use (e.g. default model). */
201
+ model?: string;
202
+ }
203
+ declare class LaylaSDK {
204
+ readonly chat: Chat;
205
+ constructor(_options?: LaylaSDKOptions);
206
+ }
207
+
208
+ export { type ChatCompletion, type ChatCompletionChunk, type ChatCompletionCreateParamsBase, type ChatCompletionCreateParamsNonStreaming, type ChatCompletionCreateParamsStreaming, ChatCompletionStream, LaylaSDK as Layla, LaylaAbortError, type LaylaApiEvent, type LaylaApiEvent_onError, type LaylaApiEvent_onMsg, type LaylaApiEvent_onMsgEnd, type LaylaApiMessage, LaylaBridgeUnavailableError, type LaylaChatMessage, type LaylaChatRole, LaylaError, LaylaSDK, type LaylaSDKOptions, LaylaSDK as default };
package/dist/index.js ADDED
@@ -0,0 +1,322 @@
1
+ // src/index.ts
2
+ var LaylaError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "LaylaError";
6
+ }
7
+ };
8
+ var LaylaAbortError = class extends LaylaError {
9
+ constructor(message = "Request was aborted") {
10
+ super(message);
11
+ this.name = "LaylaAbortError";
12
+ }
13
+ };
14
+ var LaylaBridgeUnavailableError = class extends LaylaError {
15
+ constructor() {
16
+ super(
17
+ "Layla bridge unavailable: window.ReactNativeWebView is not present. Make sure this code runs inside the Layla WebView and that the <WebView> has its `onMessage` prop set (that is what injects the bridge)."
18
+ );
19
+ this.name = "LaylaBridgeUnavailableError";
20
+ }
21
+ };
22
+ var Deferred = class {
23
+ constructor() {
24
+ this.promise = new Promise((res, rej) => {
25
+ this.resolve = res;
26
+ this.reject = rej;
27
+ });
28
+ }
29
+ };
30
+ var _LaylaBridge = class _LaylaBridge {
31
+ constructor() {
32
+ this.queue = [];
33
+ this.active = null;
34
+ this.listening = false;
35
+ this.onWindowMessage = (event) => {
36
+ const raw = event.data;
37
+ if (typeof raw !== "string") return;
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ } catch {
42
+ return;
43
+ }
44
+ if (!parsed || typeof parsed.event !== "string") return;
45
+ const job = this.active;
46
+ if (!job) return;
47
+ const sink = job.sink;
48
+ switch (parsed.event) {
49
+ case "on_message": {
50
+ const data = parsed.data ?? { msg: "", delta: "" };
51
+ sink.handleDelta(data.delta ?? "", data.msg ?? "");
52
+ break;
53
+ }
54
+ case "on_message_end": {
55
+ sink.handleEnd();
56
+ this.finishActive();
57
+ break;
58
+ }
59
+ case "on_error": {
60
+ const data = parsed.data;
61
+ sink.handleError(new LaylaError(data?.message || "Layla model error"));
62
+ this.finishActive();
63
+ break;
64
+ }
65
+ default:
66
+ break;
67
+ }
68
+ };
69
+ }
70
+ static shared() {
71
+ if (!_LaylaBridge.instance) _LaylaBridge.instance = new _LaylaBridge();
72
+ return _LaylaBridge.instance;
73
+ }
74
+ ensureListening() {
75
+ if (this.listening || typeof window === "undefined") return;
76
+ window.addEventListener("message", this.onWindowMessage);
77
+ this.listening = true;
78
+ }
79
+ enqueue(message, sink) {
80
+ this.ensureListening();
81
+ this.queue.push({ message, sink });
82
+ this.pump();
83
+ }
84
+ /** Remove a not-yet-started job (used when a queued request is aborted). */
85
+ cancelQueued(sink) {
86
+ this.queue = this.queue.filter((job) => job.sink !== sink);
87
+ }
88
+ finishActive() {
89
+ this.active = null;
90
+ this.pump();
91
+ }
92
+ pump() {
93
+ if (this.active) return;
94
+ const next = this.queue.shift();
95
+ if (!next) return;
96
+ if (next.sink.isClosed()) {
97
+ this.pump();
98
+ return;
99
+ }
100
+ this.active = next;
101
+ this.send(next);
102
+ }
103
+ send(job) {
104
+ if (typeof window === "undefined" || !window.ReactNativeWebView) {
105
+ this.active = null;
106
+ job.sink.handleError(new LaylaBridgeUnavailableError());
107
+ this.pump();
108
+ return;
109
+ }
110
+ window.ReactNativeWebView.postMessage(JSON.stringify(job.message));
111
+ }
112
+ };
113
+ _LaylaBridge.instance = null;
114
+ var LaylaBridge = _LaylaBridge;
115
+ var ChatCompletionStream = class {
116
+ constructor(model) {
117
+ this.id = `chatcmpl-layla-${Date.now()}-${Math.floor(
118
+ Math.random() * 1e6
119
+ )}`;
120
+ this.created = Math.floor(Date.now() / 1e3);
121
+ this.buffer = [];
122
+ this.resolvers = [];
123
+ this.rejectors = [];
124
+ this.snapshot = "";
125
+ this.ended = false;
126
+ this.closed = false;
127
+ this.failure = null;
128
+ this.listeners = {};
129
+ this.finalDeferred = new Deferred();
130
+ this.model = model;
131
+ this.finalDeferred.promise.catch(() => void 0);
132
+ }
133
+ on(event, listener) {
134
+ var _a;
135
+ ((_a = this.listeners)[event] || (_a[event] = [])).push(listener);
136
+ return this;
137
+ }
138
+ off(event, listener) {
139
+ const ls = this.listeners[event];
140
+ if (ls) this.listeners[event] = ls.filter((l) => l !== listener);
141
+ return this;
142
+ }
143
+ emit(event, ...args) {
144
+ const ls = this.listeners[event];
145
+ if (!ls) return;
146
+ for (const l of ls.slice()) {
147
+ try {
148
+ l(...args);
149
+ } catch {
150
+ }
151
+ }
152
+ }
153
+ /* ---- StreamSink: driven by the bridge --------------------------------- */
154
+ handleDelta(delta, snapshot) {
155
+ if (this.closed) return;
156
+ this.snapshot = snapshot || this.snapshot + delta;
157
+ const chunk = this.makeChunk({ content: delta }, null);
158
+ this.pushChunk(chunk);
159
+ this.emit("chunk", chunk);
160
+ if (delta) this.emit("content", delta, this.snapshot);
161
+ }
162
+ handleEnd() {
163
+ if (this.closed) return;
164
+ const finalChunk = this.makeChunk({}, "stop");
165
+ this.pushChunk(finalChunk);
166
+ this.emit("chunk", finalChunk);
167
+ this.ended = true;
168
+ this.closed = true;
169
+ this.drainDone();
170
+ const completion = this.buildCompletion();
171
+ this.emit("end");
172
+ this.finalDeferred.resolve(completion);
173
+ }
174
+ handleError(err) {
175
+ if (this.closed) return;
176
+ this.failure = err;
177
+ this.closed = true;
178
+ this.drainError(err);
179
+ this.emit("error", err);
180
+ this.finalDeferred.reject(err);
181
+ }
182
+ isClosed() {
183
+ return this.closed;
184
+ }
185
+ /** Abort from the consumer side. */
186
+ abort(reason) {
187
+ if (this.closed) return;
188
+ const err = reason instanceof Error ? reason : new LaylaAbortError();
189
+ LaylaBridge.shared().cancelQueued(this);
190
+ this.handleError(err);
191
+ }
192
+ /* ---- async iteration -------------------------------------------------- */
193
+ next() {
194
+ if (this.buffer.length) {
195
+ return Promise.resolve({ value: this.buffer.shift(), done: false });
196
+ }
197
+ if (this.failure) return Promise.reject(this.failure);
198
+ if (this.ended) {
199
+ return Promise.resolve({ value: void 0, done: true });
200
+ }
201
+ return new Promise((resolve, reject) => {
202
+ this.resolvers.push(resolve);
203
+ this.rejectors.push(reject);
204
+ });
205
+ }
206
+ /** Breaking out of `for await` aborts the request, like the OpenAI SDK. */
207
+ return() {
208
+ this.abort(new LaylaAbortError("Stream consumer stopped"));
209
+ return Promise.resolve({ value: void 0, done: true });
210
+ }
211
+ [Symbol.asyncIterator]() {
212
+ return this;
213
+ }
214
+ /* ---- convenience promises -------------------------------------------- */
215
+ finalChatCompletion() {
216
+ return this.finalDeferred.promise;
217
+ }
218
+ async finalContent() {
219
+ const completion = await this.finalDeferred.promise;
220
+ return completion.choices[0]?.message.content ?? "";
221
+ }
222
+ /* ---- internals -------------------------------------------------------- */
223
+ pushChunk(chunk) {
224
+ if (this.resolvers.length) {
225
+ this.resolvers.shift()({ value: chunk, done: false });
226
+ this.rejectors.shift();
227
+ } else {
228
+ this.buffer.push(chunk);
229
+ }
230
+ }
231
+ drainDone() {
232
+ while (this.resolvers.length) {
233
+ this.resolvers.shift()({ value: void 0, done: true });
234
+ this.rejectors.shift();
235
+ }
236
+ }
237
+ drainError(err) {
238
+ while (this.rejectors.length) {
239
+ this.rejectors.shift()(err);
240
+ this.resolvers.shift();
241
+ }
242
+ }
243
+ makeChunk(delta, finish) {
244
+ return {
245
+ id: this.id,
246
+ object: "chat.completion.chunk",
247
+ created: this.created,
248
+ model: this.model,
249
+ choices: [{ index: 0, delta, finish_reason: finish }]
250
+ };
251
+ }
252
+ buildCompletion() {
253
+ return {
254
+ id: this.id,
255
+ object: "chat.completion",
256
+ created: this.created,
257
+ model: this.model,
258
+ choices: [
259
+ {
260
+ index: 0,
261
+ message: { role: "assistant", content: this.snapshot },
262
+ finish_reason: "stop"
263
+ }
264
+ ]
265
+ };
266
+ }
267
+ };
268
+ var Completions = class {
269
+ create(body) {
270
+ const stream = this.startStream(
271
+ body.messages,
272
+ body.model ?? "layla",
273
+ body.signal
274
+ );
275
+ if (body.stream) return Promise.resolve(stream);
276
+ return stream.finalChatCompletion();
277
+ }
278
+ /**
279
+ * OpenAI-style helper: returns the live stream object synchronously (not a
280
+ * promise), so you can attach `.on(...)` listeners before any token arrives.
281
+ */
282
+ stream(body) {
283
+ return this.startStream(body.messages, body.model ?? "layla", body.signal);
284
+ }
285
+ startStream(messages, model, signal) {
286
+ const stream = new ChatCompletionStream(model);
287
+ if (signal?.aborted) {
288
+ queueMicrotask(() => stream.abort(new LaylaAbortError()));
289
+ return stream;
290
+ }
291
+ if (signal) {
292
+ signal.addEventListener(
293
+ "abort",
294
+ () => stream.abort(new LaylaAbortError()),
295
+ { once: true }
296
+ );
297
+ }
298
+ LaylaBridge.shared().enqueue({ cmd: "send_message", data: messages }, stream);
299
+ return stream;
300
+ }
301
+ };
302
+ var Chat = class {
303
+ constructor() {
304
+ this.completions = new Completions();
305
+ }
306
+ };
307
+ var LaylaSDK = class {
308
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
309
+ constructor(_options = {}) {
310
+ this.chat = new Chat();
311
+ }
312
+ };
313
+ var index_default = LaylaSDK;
314
+ export {
315
+ ChatCompletionStream,
316
+ LaylaSDK as Layla,
317
+ LaylaAbortError,
318
+ LaylaBridgeUnavailableError,
319
+ LaylaError,
320
+ LaylaSDK,
321
+ index_default as default
322
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@layla-network/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Layla custom mini-apps SDK",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.4.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }