@kaoruisaac/pedelec 0.1.24 → 0.1.26

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.d.ts CHANGED
@@ -224,7 +224,6 @@ export declare class Pedelec {
224
224
  private readonly sessions;
225
225
  private readonly lastSeqBySession;
226
226
  private nextRequestNumber;
227
- private disconnectedError;
228
227
  private port;
229
228
  constructor(options?: PedelecOptions);
230
229
  createSession(): Promise<PedelecSession<string>>;
@@ -241,12 +240,14 @@ export declare class Pedelec {
241
240
  private resolveProviderOnlyCreateSessionInput;
242
241
  private assertDefaultProviderAvailable;
243
242
  private registerSession;
243
+ private ensureExtensionPort;
244
244
  private connectExtension;
245
245
  private handlePortMessage;
246
246
  private handleResponse;
247
247
  private isNewEvent;
248
248
  private handleDisconnect;
249
249
  private broadcastError;
250
+ private invalidateSessions;
250
251
  }
251
252
  export declare class PedelecSession<TToolName extends string = string> {
252
253
  private readonly client;
@@ -262,6 +263,7 @@ export declare class PedelecSession<TToolName extends string = string> {
262
263
  private ending;
263
264
  private prepared;
264
265
  private activeTurn;
266
+ private transportDetached;
265
267
  private genericToolHandler;
266
268
  private inlineToolHandlers;
267
269
  private readonly namedToolHandlers;
@@ -288,6 +290,10 @@ export declare class PedelecSession<TToolName extends string = string> {
288
290
  end(): Promise<void>;
289
291
  handleEvent(event: SessionEvent, meta?: EventDispatchMeta): void;
290
292
  replaceInlineToolHandlers(handlers: Map<string, ToolSpecificHandler>): void;
293
+ /** @internal */
294
+ isTransportDetached(): boolean;
295
+ /** @internal */
296
+ handleTransportDisconnect(error: PedelecError): void;
291
297
  private handleToolCall;
292
298
  private resolvePendingSend;
293
299
  private resolvePendingPrepare;
package/dist/index.js CHANGED
@@ -122,14 +122,12 @@ export class Pedelec {
122
122
  sessions = new Map();
123
123
  lastSeqBySession = new Map();
124
124
  nextRequestNumber = 1;
125
- disconnectedError = null;
126
125
  port = null;
127
126
  constructor(options = {}) {
128
127
  this.pageWindow = typeof window === "undefined" ? null : window;
129
128
  this.channelId = createChannelId();
130
129
  this.bridgeTimeoutMs = Math.max(1, options.bridgeTimeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS);
131
130
  if (!this.pageWindow) {
132
- this.disconnectedError = makeError("EXTENSION_UNAVAILABLE", "Pedelec SDK must run in a browser page context.");
133
131
  return;
134
132
  }
135
133
  this.connectExtension();
@@ -157,14 +155,6 @@ export class Pedelec {
157
155
  return normalizePedelecSettings(await this.request("get_settings"));
158
156
  }
159
157
  async getApprovalStatus() {
160
- if (this.disconnectedError) {
161
- return {
162
- installed: false,
163
- approved: false,
164
- origin: getCurrentOrigin(this.pageWindow),
165
- appConnected: false,
166
- };
167
- }
168
158
  try {
169
159
  const result = await this.request("get_approval_status");
170
160
  return result;
@@ -195,11 +185,12 @@ export class Pedelec {
195
185
  return this.registerSession(result.sessionId, "", undefined);
196
186
  }
197
187
  request(type, payload = {}) {
198
- if (this.disconnectedError) {
199
- return Promise.reject(this.disconnectedError);
188
+ let port;
189
+ try {
190
+ port = this.ensureExtensionPort();
200
191
  }
201
- if (!this.port) {
202
- return Promise.reject(makeError("EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable."));
192
+ catch (err) {
193
+ return Promise.reject(normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable."));
203
194
  }
204
195
  const requestId = `sdk_${Date.now()}_${this.nextRequestNumber++}`;
205
196
  const message = { channelId: this.channelId, type, requestId, ...payload };
@@ -212,17 +203,22 @@ export class Pedelec {
212
203
  }));
213
204
  }, this.bridgeTimeoutMs);
214
205
  this.pendingRequests.set(requestId, {
206
+ port,
215
207
  resolve: (value) => resolve(value),
216
208
  reject,
217
209
  timeoutId,
218
210
  });
219
211
  try {
220
- this.port?.postMessage(message);
212
+ port.postMessage(message);
221
213
  }
222
214
  catch (err) {
223
215
  this.pendingRequests.delete(requestId);
224
216
  clearTimeout(timeoutId);
225
- reject(normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected."));
217
+ const error = normalizeError(err, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
218
+ if (this.port === port) {
219
+ this.handleDisconnect(port, error);
220
+ }
221
+ reject(error);
226
222
  }
227
223
  });
228
224
  }
@@ -341,7 +337,7 @@ export class Pedelec {
341
337
  }
342
338
  registerSession(sessionId, provider, model, inlineToolHandlers = new Map()) {
343
339
  const existing = this.sessions.get(sessionId);
344
- if (existing) {
340
+ if (existing && !existing.isTransportDetached()) {
345
341
  existing.replaceInlineToolHandlers(inlineToolHandlers);
346
342
  return existing;
347
343
  }
@@ -349,23 +345,40 @@ export class Pedelec {
349
345
  this.sessions.set(sessionId, session);
350
346
  return session;
351
347
  }
352
- connectExtension() {
348
+ ensureExtensionPort() {
349
+ if (this.port)
350
+ return this.port;
351
+ if (!this.pageWindow) {
352
+ throw makeError("EXTENSION_UNAVAILABLE", "Pedelec SDK must run in a browser page context.");
353
+ }
353
354
  const runtime = globalThis.chrome?.runtime;
354
355
  if (!runtime?.connect) {
355
- this.disconnectedError = makeError("EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
356
- return;
356
+ throw makeError("EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
357
357
  }
358
358
  try {
359
- this.port = runtime.connect(PEDELEC_EXTENSION_ID, { name: SDK_EXTERNAL_PORT_NAME });
360
- this.port.onMessage.addListener((message) => this.handlePortMessage(message));
361
- this.port.onDisconnect.addListener(() => this.handleDisconnect());
359
+ const port = runtime.connect(PEDELEC_EXTENSION_ID, { name: SDK_EXTERNAL_PORT_NAME });
360
+ port.onMessage.addListener((message) => this.handlePortMessage(port, message));
361
+ port.onDisconnect.addListener(() => this.handleDisconnect(port));
362
+ this.port = port;
363
+ return port;
362
364
  }
363
365
  catch (err) {
364
- this.disconnectedError = normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
365
366
  this.port = null;
367
+ throw normalizeError(err, "EXTENSION_UNAVAILABLE", "Pedelec extension is unavailable.");
368
+ }
369
+ }
370
+ connectExtension() {
371
+ try {
372
+ this.ensureExtensionPort();
373
+ }
374
+ catch (_err) {
375
+ // The first connection is eager for normal browser startup, but a failed
376
+ // attempt remains retryable through the next request.
366
377
  }
367
378
  }
368
- handlePortMessage(raw) {
379
+ handlePortMessage(port, raw) {
380
+ if (port !== this.port)
381
+ return;
369
382
  const message = raw;
370
383
  if (!message || typeof message !== "object")
371
384
  return;
@@ -411,23 +424,31 @@ export class Pedelec {
411
424
  this.lastSeqBySession.set(event.sessionId, event.seq);
412
425
  return true;
413
426
  }
414
- handleDisconnect() {
427
+ handleDisconnect(port, disconnectError) {
428
+ if (port !== this.port)
429
+ return;
415
430
  const runtime = globalThis.chrome?.runtime;
416
- const error = normalizeError(runtime?.lastError, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
417
- this.disconnectedError = error;
431
+ const error = normalizeError(disconnectError ?? runtime?.lastError, "EXTENSION_DISCONNECTED", "Pedelec extension disconnected.");
418
432
  this.port = null;
419
- for (const pending of this.pendingRequests.values()) {
433
+ this.invalidateSessions(error);
434
+ for (const [requestId, pending] of this.pendingRequests) {
435
+ if (pending.port !== port)
436
+ continue;
437
+ this.pendingRequests.delete(requestId);
420
438
  clearTimeout(pending.timeoutId);
421
439
  pending.reject(error);
422
440
  }
423
- this.pendingRequests.clear();
424
- this.broadcastError(error);
425
441
  }
426
442
  broadcastError(error) {
427
443
  for (const session of this.sessions.values()) {
428
444
  session.handleEvent({ type: "error", sessionId: session.sessionId, error }, { source: "sdk" });
429
445
  }
430
446
  }
447
+ invalidateSessions(error) {
448
+ for (const session of this.sessions.values()) {
449
+ session.handleTransportDisconnect(error);
450
+ }
451
+ }
431
452
  }
432
453
  export class PedelecSession {
433
454
  client;
@@ -443,6 +464,7 @@ export class PedelecSession {
443
464
  ending = false;
444
465
  prepared = false;
445
466
  activeTurn = null;
467
+ transportDetached = false;
446
468
  genericToolHandler = null;
447
469
  inlineToolHandlers = new Map();
448
470
  namedToolHandlers = new Map();
@@ -458,7 +480,7 @@ export class PedelecSession {
458
480
  this.inlineToolHandlers = new Map(inlineToolHandlers);
459
481
  }
460
482
  prepare() {
461
- if (this.status === "ended") {
483
+ if (this.transportDetached || this.status === "ended") {
462
484
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
463
485
  }
464
486
  if (this.pendingSend) {
@@ -482,7 +504,8 @@ export class PedelecSession {
482
504
  })
483
505
  .catch((err) => {
484
506
  const error = normalizeError(err, "PREPARE_FAILED", "prepare failed");
485
- this.emitError(error, { source: "sdk" });
507
+ if (!this.transportDetached)
508
+ this.emitError(error, { source: "sdk" });
486
509
  this.clearPendingPrepare();
487
510
  this.finishActiveTurn(turn);
488
511
  throw error;
@@ -505,7 +528,7 @@ export class PedelecSession {
505
528
  // prepare is an optimization; sendText falls back to the original first-run path.
506
529
  }
507
530
  }
508
- if (this.status === "ended") {
531
+ if (this.transportDetached || this.status === "ended") {
509
532
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
510
533
  }
511
534
  if (this.pendingSend || this.pendingPrepare) {
@@ -524,7 +547,8 @@ export class PedelecSession {
524
547
  })
525
548
  .catch((err) => {
526
549
  const error = normalizeError(err, "SEND_TEXT_FAILED", "sendText failed");
527
- this.emitError(error, { source: "sdk" });
550
+ if (!this.transportDetached)
551
+ this.emitError(error, { source: "sdk" });
528
552
  this.clearPendingSend();
529
553
  this.finishActiveTurn(turn);
530
554
  throw error;
@@ -560,7 +584,7 @@ export class PedelecSession {
560
584
  };
561
585
  }
562
586
  uploadAsset(file, targetPath) {
563
- if (this.ending || this.status === "ended")
587
+ if (this.transportDetached || this.ending || this.status === "ended")
564
588
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
565
589
  if (this.uploadPromise) {
566
590
  return Promise.reject(makeError("SESSION_BUSY", "session is busy", { sessionId: this.sessionId }));
@@ -601,13 +625,13 @@ export class PedelecSession {
601
625
  return this.uploadPromise;
602
626
  }
603
627
  listAssets() {
604
- if (this.ending || this.status === "ended") {
628
+ if (this.transportDetached || this.ending || this.status === "ended") {
605
629
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
606
630
  }
607
631
  return this.client.request("list_assets", { sessionId: this.sessionId }).then(normalizeListAssetsResponse);
608
632
  }
609
633
  readAsset(path, type) {
610
- if (this.ending || this.status === "ended")
634
+ if (this.transportDetached || this.ending || this.status === "ended")
611
635
  return Promise.reject(makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId }));
612
636
  if (!isValidAssetPath(path) || !["text", "json", "file"].includes(type)) {
613
637
  return Promise.reject(makeError("ASSET_PATH_INVALID", "asset path or read type is invalid"));
@@ -671,8 +695,12 @@ export class PedelecSession {
671
695
  return this.status;
672
696
  }
673
697
  async end() {
674
- if (this.status === "ended" || this.ending)
698
+ if (this.transportDetached || this.status === "ended" || this.ending) {
699
+ if (this.transportDetached) {
700
+ throw makeError("SESSION_ENDED", "session has ended", { sessionId: this.sessionId });
701
+ }
675
702
  return;
703
+ }
676
704
  this.ending = true;
677
705
  try {
678
706
  await this.client.request("end_session", {
@@ -742,6 +770,19 @@ export class PedelecSession {
742
770
  replaceInlineToolHandlers(handlers) {
743
771
  this.inlineToolHandlers = new Map(handlers);
744
772
  }
773
+ /** @internal */
774
+ isTransportDetached() {
775
+ return this.transportDetached;
776
+ }
777
+ /** @internal */
778
+ handleTransportDisconnect(error) {
779
+ if (this.transportDetached)
780
+ return;
781
+ this.transportDetached = true;
782
+ this.setStatus("error", { source: "sdk" });
783
+ this.emitError(error, { source: "sdk" });
784
+ this.rejectActivePending(error);
785
+ }
745
786
  async handleToolCall(event, meta, turn) {
746
787
  let result;
747
788
  const namedHandler = this.namedToolHandlers.get(event.tool);
@@ -783,6 +824,8 @@ export class PedelecSession {
783
824
  }),
784
825
  };
785
826
  }
827
+ if (this.transportDetached)
828
+ return;
786
829
  try {
787
830
  await this.client.request("submit_tool_result", {
788
831
  sessionId: this.sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaoruisaac/pedelec",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",