@ai-ecoverse/sliccy-browser 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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/browser.js +112 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/sliccy-browser",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "sliccy:browser for jsh, over CDP",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,6 +29,6 @@
29
29
  "src"
30
30
  ],
31
31
  "dependencies": {
32
- "@ai-ecoverse/slicc-cdp": "1.5.4"
32
+ "@ai-ecoverse/slicc-cdp": "1.19.1"
33
33
  }
34
34
  }
package/src/browser.js CHANGED
@@ -13,6 +13,8 @@ const routers = new Set();
13
13
  const subscribers = new Map();
14
14
  let seq = 1;
15
15
  let listening = false;
16
+ let active = 0;
17
+ let idle;
16
18
 
17
19
  function openClient() {
18
20
  if (!opening) {
@@ -24,6 +26,37 @@ function openClient() {
24
26
  return opening;
25
27
  }
26
28
 
29
+ async function using(work) {
30
+ active += 1;
31
+ clearTimeout(idle);
32
+ idle = undefined;
33
+ try {
34
+ return await work();
35
+ } finally {
36
+ active -= 1;
37
+ release();
38
+ }
39
+ }
40
+
41
+ function release() {
42
+ if (active > 0 || subscribers.size > 0 || !opening || idle) return;
43
+ idle = setTimeout(closeIdle, 0);
44
+ }
45
+
46
+ function closeIdle() {
47
+ idle = undefined;
48
+ if (active > 0 || subscribers.size > 0 || !opening) return;
49
+ const closing = opening;
50
+ opening = undefined;
51
+ sessions.clear();
52
+ routers.clear();
53
+ listening = false;
54
+ closing.then(
55
+ (client) => client.close(),
56
+ () => undefined
57
+ );
58
+ }
59
+
27
60
  function ignoreMethod(prefix) {
28
61
  return (error) => {
29
62
  const message = error instanceof Error ? error.message : String(error);
@@ -113,7 +146,11 @@ async function listTabs() {
113
146
  }));
114
147
  }
115
148
 
116
- export async function findTab(query = {}) {
149
+ export async function findTab(query) {
150
+ return using(() => findTabNow(query));
151
+ }
152
+
153
+ async function findTabNow(query = {}) {
117
154
  const domain = typeof query.domain === 'string' ? query.domain : undefined;
118
155
  const urlMatch = patternOf(query.urlMatch);
119
156
  if (domain === undefined && urlMatch === undefined) {
@@ -128,7 +165,11 @@ export async function findTab(query = {}) {
128
165
  return tabs.find((tab) => re.test(tab.url)) ?? null;
129
166
  }
130
167
 
131
- export async function ensureTab(url, options = {}) {
168
+ export async function ensureTab(url, options) {
169
+ return using(() => ensureTabNow(url, options));
170
+ }
171
+
172
+ async function ensureTabNow(url, options = {}) {
132
173
  if (typeof url !== 'string' || url.length === 0) {
133
174
  throw new TypeError('browser.ensureTab: url must be a string');
134
175
  }
@@ -146,13 +187,14 @@ export async function ensureTab(url, options = {}) {
146
187
  }
147
188
  }
148
189
  const client = await openClient();
149
- const created = await client.send('Target.createTarget', { url });
190
+ const created = await client.send('Target.createTarget', { url: 'about:blank' });
150
191
  const targetId = created?.targetId;
151
192
  if (typeof targetId !== 'string' || targetId.length === 0) {
152
193
  throw new Error('Target.createTarget returned no targetId');
153
194
  }
154
195
  const { sessionId } = await sessionFor(targetId);
155
- await waitForLoad(client, sessionId);
196
+ if (url === 'about:blank') await waitForLoad(client, sessionId);
197
+ else await navigateAndWait(client, sessionId, url);
156
198
  const live = (await listTabs()).find((tab) => tab.targetId === targetId);
157
199
  return live ?? { targetId, url, title: '' };
158
200
  }
@@ -167,7 +209,11 @@ function evalSourceAsync(tab, source) {
167
209
 
168
210
  export { evalSource as eval, evalSourceAsync as evalAsync };
169
211
 
170
- async function evaluate(tab, source, awaitPromise) {
212
+ function evaluate(tab, source, awaitPromise) {
213
+ return using(() => evaluateNow(tab, source, awaitPromise));
214
+ }
215
+
216
+ async function evaluateNow(tab, source, awaitPromise) {
171
217
  const { client, sessionId } = await sessionFor(targetIdOf(tab));
172
218
  const result = await client.send(
173
219
  'Runtime.evaluate',
@@ -186,6 +232,10 @@ async function evaluate(tab, source, awaitPromise) {
186
232
 
187
233
  export async function cookie(tab, name) {
188
234
  if (typeof name !== 'string') throw new TypeError('browser.cookie: name must be a string');
235
+ return using(() => cookieNow(tab, name));
236
+ }
237
+
238
+ async function cookieNow(tab, name) {
189
239
  const { client, sessionId } = await sessionFor(targetIdOf(tab));
190
240
  const result = await client.send('Network.getCookies', {}, sessionId);
191
241
  const cookies = Array.isArray(result?.cookies) ? result.cookies : [];
@@ -284,6 +334,34 @@ function pageIsSettled(client, sessionId) {
284
334
  });
285
335
  }
286
336
 
337
+ function navigateAndWait(client, sessionId, url) {
338
+ return new Promise((resolve, reject) => {
339
+ let settled = false;
340
+ let timer;
341
+ const finish = (error) => {
342
+ if (settled) return;
343
+ settled = true;
344
+ clearTimeout(timer);
345
+ client.off('Page.loadEventFired', listener);
346
+ if (error) reject(error);
347
+ else resolve();
348
+ };
349
+ const listener = (message) => {
350
+ if (typeof message?.sessionId === 'string' && message.sessionId !== sessionId) return;
351
+ finish();
352
+ };
353
+ client.on('Page.loadEventFired', listener);
354
+ timer = setTimeout(() => finish(), LOAD_TIMEOUT_MS);
355
+ client.send('Page.navigate', { url }, sessionId).then(
356
+ (result) => {
357
+ if (result?.errorText) finish(new Error(`navigate: ${result.errorText}`));
358
+ else if (!result?.loaderId) finish();
359
+ },
360
+ (error) => finish(error)
361
+ );
362
+ });
363
+ }
364
+
287
365
  function waitForLoad(client, sessionId) {
288
366
  return new Promise((resolve, reject) => {
289
367
  let settled = false;
@@ -396,9 +474,11 @@ function makeHandle(sub) {
396
474
  : patch.urlMatch;
397
475
  }
398
476
  if (patch.filter !== undefined) wire.filter = patch.filter;
399
- await callRouter(
400
- sub.targetId,
401
- `globalThis.__sliccWsRouter.update(${JSON.stringify(sub.id)}, ${JSON.stringify(wire)})`
477
+ await using(() =>
478
+ callRouter(
479
+ sub.targetId,
480
+ `globalThis.__sliccWsRouter.update(${JSON.stringify(sub.id)}, ${JSON.stringify(wire)})`
481
+ )
402
482
  );
403
483
  if (wire.urlMatch === null) delete sub.urlMatch;
404
484
  else if (wire.urlMatch !== undefined) sub.urlMatch = wire.urlMatch;
@@ -409,9 +489,8 @@ function makeHandle(sub) {
409
489
  async close() {
410
490
  if (!subscribers.has(sub.id)) return false;
411
491
  subscribers.delete(sub.id);
412
- await callRouter(
413
- sub.targetId,
414
- `globalThis.__sliccWsRouter.unregister(${JSON.stringify(sub.id)})`
492
+ await using(() =>
493
+ callRouter(sub.targetId, `globalThis.__sliccWsRouter.unregister(${JSON.stringify(sub.id)})`)
415
494
  );
416
495
  return true;
417
496
  },
@@ -452,27 +531,30 @@ export const websocket = {
452
531
  },
453
532
  async forward(sink) {
454
533
  validateSink(sink);
455
- await installRouter(targetId);
456
- const id = `wssub-${seq++}-${Date.now().toString(36)}`;
457
- const sub = {
458
- id,
459
- targetId,
460
- forward: sink,
461
- createdAt: new Date().toISOString(),
462
- };
463
- if (urlMatch !== undefined) sub.urlMatch = urlMatch;
464
- if (selector !== undefined) sub.filter = selector;
465
- const registered = { id };
466
- if (urlMatch !== undefined) registered.urlMatch = urlMatch;
467
- if (selector !== undefined) registered.filter = selector;
468
- await callRouter(
469
- targetId,
470
- `globalThis.__sliccWsRouter.register(${JSON.stringify(registered)})`
471
- );
472
- subscribers.set(id, sub);
473
- return makeHandle(sub);
534
+ return using(() => subscribe(sink));
474
535
  },
475
536
  };
537
+ async function subscribe(sink) {
538
+ await installRouter(targetId);
539
+ const id = `wssub-${seq++}-${Date.now().toString(36)}`;
540
+ const sub = {
541
+ id,
542
+ targetId,
543
+ forward: sink,
544
+ createdAt: new Date().toISOString(),
545
+ };
546
+ if (urlMatch !== undefined) sub.urlMatch = urlMatch;
547
+ if (selector !== undefined) sub.filter = selector;
548
+ const registered = { id };
549
+ if (urlMatch !== undefined) registered.urlMatch = urlMatch;
550
+ if (selector !== undefined) registered.filter = selector;
551
+ await callRouter(
552
+ targetId,
553
+ `globalThis.__sliccWsRouter.register(${JSON.stringify(registered)})`
554
+ );
555
+ subscribers.set(id, sub);
556
+ return makeHandle(sub);
557
+ }
476
558
  return builder;
477
559
  },
478
560
  async list() {