@layla-network/sdk 0.1.0 → 0.1.1

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.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/index.ts
1
+ // src/errors.ts
2
2
  var LaylaError = class extends Error {
3
3
  constructor(message) {
4
4
  super(message);
@@ -19,14 +19,8 @@ var LaylaBridgeUnavailableError = class extends LaylaError {
19
19
  this.name = "LaylaBridgeUnavailableError";
20
20
  }
21
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
- };
22
+
23
+ // src/internal/bridge.ts
30
24
  var _LaylaBridge = class _LaylaBridge {
31
25
  constructor() {
32
26
  this.queue = [];
@@ -44,27 +38,13 @@ var _LaylaBridge = class _LaylaBridge {
44
38
  if (!parsed || typeof parsed.event !== "string") return;
45
39
  const job = this.active;
46
40
  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;
41
+ if (parsed.event === "on_error") {
42
+ const data = parsed.data;
43
+ job.sink.fail(new LaylaError(data?.message || "Layla model error"));
44
+ this.finishActive();
45
+ return;
67
46
  }
47
+ if (job.sink.accept(parsed)) this.finishActive();
68
48
  };
69
49
  }
70
50
  static shared() {
@@ -76,14 +56,33 @@ var _LaylaBridge = class _LaylaBridge {
76
56
  window.addEventListener("message", this.onWindowMessage);
77
57
  this.listening = true;
78
58
  }
79
- enqueue(message, sink) {
59
+ enqueue(job) {
80
60
  this.ensureListening();
81
- this.queue.push({ message, sink });
61
+ this.queue.push(job);
82
62
  this.pump();
83
63
  }
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);
64
+ /**
65
+ * Cancel a request.
66
+ *
67
+ * - If still queued (not yet sent), drop it; the host never sees it.
68
+ * - If it is the active (in-flight) request, ask the sink for a stop message
69
+ * and post it (streaming generation -> `{ cmd: 'cancel' }`; one-shot -> none).
70
+ * We deliberately do NOT free the active slot here: it frees when the host
71
+ * sends the request's terminating event (`on_message_end` /
72
+ * `on_get_characters_response` / `on_error`). Holding the slot until then
73
+ * keeps any trailing event attributed to the (already-closed) sink — which
74
+ * swallows it — instead of leaking into the next request.
75
+ */
76
+ cancel(sink) {
77
+ const remaining = this.queue.filter((job) => job.sink !== sink);
78
+ if (remaining.length !== this.queue.length) {
79
+ this.queue = remaining;
80
+ return;
81
+ }
82
+ if (this.active && this.active.sink === sink) {
83
+ const stop = sink.cancelMessage?.();
84
+ if (stop) this.post(stop);
85
+ }
87
86
  }
88
87
  finishActive() {
89
88
  this.active = null;
@@ -101,17 +100,33 @@ var _LaylaBridge = class _LaylaBridge {
101
100
  this.send(next);
102
101
  }
103
102
  send(job) {
104
- if (typeof window === "undefined" || !window.ReactNativeWebView) {
103
+ if (!this.post(job.message)) {
105
104
  this.active = null;
106
- job.sink.handleError(new LaylaBridgeUnavailableError());
105
+ job.sink.fail(new LaylaBridgeUnavailableError());
107
106
  this.pump();
108
- return;
109
107
  }
110
- window.ReactNativeWebView.postMessage(JSON.stringify(job.message));
108
+ }
109
+ /** Post a message to the host. Returns false if the bridge isn't present. */
110
+ post(message) {
111
+ if (typeof window === "undefined" || !window.ReactNativeWebView) return false;
112
+ window.ReactNativeWebView.postMessage(JSON.stringify(message));
113
+ return true;
111
114
  }
112
115
  };
113
116
  _LaylaBridge.instance = null;
114
117
  var LaylaBridge = _LaylaBridge;
118
+
119
+ // src/internal/deferred.ts
120
+ var Deferred = class {
121
+ constructor() {
122
+ this.promise = new Promise((res, rej) => {
123
+ this.resolve = res;
124
+ this.reject = rej;
125
+ });
126
+ }
127
+ };
128
+
129
+ // src/resources/chat/stream.ts
115
130
  var ChatCompletionStream = class {
116
131
  constructor(model) {
117
132
  this.id = `chatcmpl-layla-${Date.now()}-${Math.floor(
@@ -150,7 +165,43 @@ var ChatCompletionStream = class {
150
165
  }
151
166
  }
152
167
  }
153
- /* ---- StreamSink: driven by the bridge --------------------------------- */
168
+ /* ---- BridgeSink: driven by the bridge --------------------------------- */
169
+ accept(event) {
170
+ switch (event.event) {
171
+ case "on_message": {
172
+ const data = event.data ?? { msg: "", delta: "" };
173
+ this.handleDelta(data.delta ?? "", data.msg ?? "");
174
+ return false;
175
+ }
176
+ case "on_message_end":
177
+ this.handleEnd();
178
+ return true;
179
+ // terminal
180
+ default:
181
+ return false;
182
+ }
183
+ }
184
+ fail(err) {
185
+ if (this.closed) return;
186
+ this.failure = err;
187
+ this.closed = true;
188
+ this.drainError(err);
189
+ this.emit("error", err);
190
+ this.finalDeferred.reject(err);
191
+ }
192
+ isClosed() {
193
+ return this.closed;
194
+ }
195
+ cancelMessage() {
196
+ return { cmd: "cancel" };
197
+ }
198
+ /** Abort from the consumer side. */
199
+ abort(reason) {
200
+ if (this.closed) return;
201
+ const err = reason instanceof Error ? reason : new LaylaAbortError();
202
+ this.fail(err);
203
+ LaylaBridge.shared().cancel(this);
204
+ }
154
205
  handleDelta(delta, snapshot) {
155
206
  if (this.closed) return;
156
207
  this.snapshot = snapshot || this.snapshot + delta;
@@ -171,24 +222,6 @@ var ChatCompletionStream = class {
171
222
  this.emit("end");
172
223
  this.finalDeferred.resolve(completion);
173
224
  }
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
225
  /* ---- async iteration -------------------------------------------------- */
193
226
  next() {
194
227
  if (this.buffer.length) {
@@ -265,6 +298,8 @@ var ChatCompletionStream = class {
265
298
  };
266
299
  }
267
300
  };
301
+
302
+ // src/resources/chat/index.ts
268
303
  var Completions = class {
269
304
  create(body) {
270
305
  const stream = this.startStream(
@@ -295,7 +330,10 @@ var Completions = class {
295
330
  { once: true }
296
331
  );
297
332
  }
298
- LaylaBridge.shared().enqueue({ cmd: "send_message", data: messages }, stream);
333
+ LaylaBridge.shared().enqueue({
334
+ message: { cmd: "send_message", data: messages },
335
+ sink: stream
336
+ });
299
337
  return stream;
300
338
  }
301
339
  };
@@ -304,19 +342,216 @@ var Chat = class {
304
342
  this.completions = new Completions();
305
343
  }
306
344
  };
345
+
346
+ // src/resources/images.ts
347
+ var ImagesBridgeSink = class {
348
+ constructor() {
349
+ this.listeners = {};
350
+ this.closed = false;
351
+ }
352
+ on(event, listener) {
353
+ var _a;
354
+ ((_a = this.listeners)[event] || (_a[event] = [])).push(listener);
355
+ return this;
356
+ }
357
+ off(event, listener) {
358
+ const ls = this.listeners[event];
359
+ if (ls) this.listeners[event] = ls.filter((l) => l !== listener);
360
+ return this;
361
+ }
362
+ accept(event) {
363
+ switch (event.event) {
364
+ case "on_generate_image_progress":
365
+ for (const l of this.listeners["on_generate_image_progress"] ?? []) {
366
+ try {
367
+ l(event.data.status, event.data.steps, event.data.total_steps);
368
+ } catch {
369
+ }
370
+ }
371
+ return false;
372
+ // not terminal
373
+ case "on_generate_image_response":
374
+ for (const l of this.listeners["on_generate_image_response"] ?? []) {
375
+ try {
376
+ l(event.data?.image_data_base64 || null);
377
+ } catch {
378
+ }
379
+ }
380
+ return true;
381
+ // terminal
382
+ default:
383
+ return false;
384
+ }
385
+ }
386
+ fail(_) {
387
+ if (this.closed) return;
388
+ this.closed = true;
389
+ }
390
+ isClosed() {
391
+ return this.closed;
392
+ }
393
+ cancelMessage() {
394
+ return null;
395
+ }
396
+ /** Abort from the consumer side. */
397
+ abort(reason) {
398
+ const err = reason instanceof Error ? reason : new LaylaAbortError();
399
+ this.fail(err);
400
+ LaylaBridge.shared().cancel(this);
401
+ }
402
+ };
403
+ var Images = class {
404
+ /**
405
+ * Ask the native host to generate an image. Resolves to a ready-to-use base64 image src string, or null if the character has no image
406
+ */
407
+ generateImage(prompt, onProgress, options = {}) {
408
+ const setupSince = () => {
409
+ const sink = new ImagesBridgeSink();
410
+ if (options.signal?.aborted) {
411
+ queueMicrotask(() => sink.abort(new LaylaAbortError()));
412
+ return sink;
413
+ }
414
+ if (options.signal) {
415
+ options.signal.addEventListener(
416
+ "abort",
417
+ () => sink.abort(new LaylaAbortError()),
418
+ { once: true }
419
+ );
420
+ }
421
+ LaylaBridge.shared().enqueue({
422
+ message: { cmd: "generate_image", data: { prompt } },
423
+ sink
424
+ });
425
+ return sink;
426
+ };
427
+ return new Promise((resolve, reject) => {
428
+ const sink = setupSince();
429
+ sink.on("on_generate_image_response", (image_data_base64) => {
430
+ resolve(image_data_base64);
431
+ });
432
+ sink.on("on_generate_image_progress", (status, step, total_step) => {
433
+ onProgress(status, step, total_step);
434
+ });
435
+ const onFailure = (err) => {
436
+ reject(err);
437
+ };
438
+ options.signal?.addEventListener("abort", () => {
439
+ onFailure(new LaylaAbortError());
440
+ });
441
+ });
442
+ }
443
+ };
444
+
445
+ // src/internal/one-shot.ts
446
+ var OneShotRequest = class {
447
+ constructor(command, responseEvent, extract) {
448
+ this.responseEvent = responseEvent;
449
+ this.extract = extract;
450
+ this.closed = false;
451
+ this.deferred = new Deferred();
452
+ this.deferred.promise.catch(() => void 0);
453
+ this.job = { message: command, sink: this };
454
+ }
455
+ get promise() {
456
+ return this.deferred.promise;
457
+ }
458
+ accept(event) {
459
+ if (event.event !== this.responseEvent) return false;
460
+ if (!this.closed) {
461
+ this.closed = true;
462
+ try {
463
+ this.deferred.resolve(this.extract(event));
464
+ } catch (err) {
465
+ this.deferred.reject(
466
+ err instanceof Error ? err : new LaylaError(String(err))
467
+ );
468
+ }
469
+ }
470
+ return true;
471
+ }
472
+ fail(err) {
473
+ if (this.closed) return;
474
+ this.closed = true;
475
+ this.deferred.reject(err);
476
+ }
477
+ isClosed() {
478
+ return this.closed;
479
+ }
480
+ // No cancelMessage(): a one-shot has no generation to stop on the host side.
481
+ /** Abort from the consumer side. */
482
+ abort(reason) {
483
+ if (this.closed) return;
484
+ const err = reason instanceof Error ? reason : new LaylaAbortError();
485
+ this.fail(err);
486
+ LaylaBridge.shared().cancel(this);
487
+ }
488
+ };
489
+ function oneShot(command, responseEvent, extract, signal) {
490
+ const req = new OneShotRequest(command, responseEvent, extract);
491
+ if (signal?.aborted) {
492
+ queueMicrotask(() => req.abort(new LaylaAbortError()));
493
+ return req.promise;
494
+ }
495
+ if (signal) {
496
+ signal.addEventListener("abort", () => req.abort(new LaylaAbortError()), {
497
+ once: true
498
+ });
499
+ }
500
+ LaylaBridge.shared().enqueue(req.job);
501
+ return req.promise;
502
+ }
503
+
504
+ // src/resources/characters.ts
505
+ var Characters = class {
506
+ /**
507
+ * Ask the native host for the available character cards. Resolves once with
508
+ * the host's `on_get_characters_response` payload, or rejects on error/abort.
509
+ */
510
+ list(options = {}) {
511
+ return oneShot(
512
+ { cmd: "get_characters" },
513
+ "on_get_characters_response",
514
+ (event) => {
515
+ const data = event.data;
516
+ return Array.isArray(data) ? data : [];
517
+ },
518
+ options.signal
519
+ );
520
+ }
521
+ /**
522
+ * Ask the native host for a character portrait. Resolves to a ready-to-use
523
+ * image src string, or null when the character has no image.
524
+ */
525
+ getImage(characterId, options = {}) {
526
+ return oneShot(
527
+ { cmd: "get_character_image", data: { character_id: characterId } },
528
+ "on_get_character_image_response",
529
+ (event) => {
530
+ const data = event.data;
531
+ return data?.image_data_base64 ?? null;
532
+ },
533
+ options.signal
534
+ );
535
+ }
536
+ };
537
+
538
+ // src/client.ts
307
539
  var LaylaSDK = class {
308
540
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
309
541
  constructor(_options = {}) {
310
542
  this.chat = new Chat();
543
+ this.characters = new Characters();
544
+ this.images = new Images();
311
545
  }
312
546
  };
313
- var index_default = LaylaSDK;
547
+ var client_default = LaylaSDK;
314
548
  export {
315
549
  ChatCompletionStream,
550
+ Images,
316
551
  LaylaSDK as Layla,
317
552
  LaylaAbortError,
318
553
  LaylaBridgeUnavailableError,
319
554
  LaylaError,
320
555
  LaylaSDK,
321
- index_default as default
556
+ client_default as default
322
557
  };
package/package.json CHANGED
@@ -1,32 +1,43 @@
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
- }
1
+ {
2
+ "name": "@layla-network/sdk",
3
+ "version": "0.1.1",
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
+ "dependencies": {
33
+ "vite-plugin-singlefile": "^2.3.3"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/l3utterfly/layla-sdk.git"
38
+ },
39
+ "homepage": "https://github.com/l3utterfly/layla-sdk#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/l3utterfly/layla-sdk/issues"
42
+ }
43
+ }