@cap.js/widget 0.1.37 → 0.1.38

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/src/cap.js CHANGED
@@ -64,7 +64,7 @@
64
64
 
65
65
  var compressed = b64ToUint8(instrBytes);
66
66
 
67
- const scriptText = await new Promise(function (resolve, reject) {
67
+ const scriptText = await new Promise((resolve, reject) => {
68
68
  try {
69
69
  var ds = new DecompressionStream("deflate-raw");
70
70
  var writer = ds.writable.getWriter();
@@ -89,7 +89,7 @@
89
89
  reader.read().then(pump).catch(reject);
90
90
  writer
91
91
  .write(compressed)
92
- .then(function () {
92
+ .then(() => {
93
93
  writer.close();
94
94
  })
95
95
  .catch(reject);
@@ -98,8 +98,8 @@
98
98
  }
99
99
  });
100
100
 
101
- return new Promise(function (resolve) {
102
- var timeout = setTimeout(function () {
101
+ return new Promise((resolve) => {
102
+ var timeout = setTimeout(() => {
103
103
  cleanup();
104
104
  resolve({ __timeout: true });
105
105
  }, 20000);
@@ -125,7 +125,10 @@
125
125
  if (d.type === "cap:instr") {
126
126
  cleanup();
127
127
  if (d.blocked) {
128
- resolve({ __blocked: true, blockReason: d.blockReason || "automated_browser" });
128
+ resolve({
129
+ __blocked: true,
130
+ blockReason: d.blockReason || "automated_browser",
131
+ });
129
132
  } else if (d.result) {
130
133
  resolve(d.result);
131
134
  } else {
@@ -179,8 +182,382 @@
179
182
  const prefersReducedMotion = () =>
180
183
  window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
181
184
 
185
+ const SPECULATIVE_DELAY_MS = 2500;
186
+ const SPECULATIVE_WORKERS = 1;
187
+ const SPECULATIVE_YIELD_MS = 120;
188
+
189
+ const speculative = {
190
+ state: "idle",
191
+ challengeResp: null,
192
+ challenges: null,
193
+ results: [],
194
+ completedCount: 0,
195
+ solvePromise: null,
196
+ promoteFn: null,
197
+ _listeners: [],
198
+ pendingPromotion: null,
199
+ token: null,
200
+ tokenExpires: null,
201
+
202
+ notify() {
203
+ for (const fn of this._listeners) fn();
204
+ this._listeners = [];
205
+ },
206
+
207
+ onSettled(fn) {
208
+ if (this.state === "done" || this.state === "error") {
209
+ fn();
210
+ } else {
211
+ this._listeners.push(fn);
212
+ }
213
+ },
214
+ };
215
+
216
+ function _resetSpeculativeState() {
217
+ speculative.state = "idle";
218
+ speculative.challengeResp = null;
219
+ speculative.challenges = null;
220
+ speculative.results = [];
221
+ speculative.completedCount = 0;
222
+ speculative.solvePromise = null;
223
+ speculative.promoteFn = null;
224
+ speculative.pendingPromotion = null;
225
+ speculative._listeners = [];
226
+ speculative.token = null;
227
+ speculative.tokenExpires = null;
228
+
229
+ _attachInteractionListeners();
230
+ }
231
+
232
+ let _speculativeTimer = null;
233
+
234
+ function _onFirstInteraction() {
235
+ if (speculative.state !== "idle") return;
236
+ speculative.state = "waiting";
237
+
238
+ _speculativeTimer = setTimeout(() => {
239
+ _beginSpeculativeSolve();
240
+ }, SPECULATIVE_DELAY_MS);
241
+ }
242
+
243
+ let _currentInteractionHandler = null;
244
+
245
+ function _detachInteractionListeners() {
246
+ if (_currentInteractionHandler) {
247
+ window.removeEventListener("mousemove", _currentInteractionHandler);
248
+ window.removeEventListener("touchstart", _currentInteractionHandler);
249
+ window.removeEventListener("keydown", _currentInteractionHandler);
250
+ _currentInteractionHandler = null;
251
+ }
252
+ }
253
+
254
+ function _attachInteractionListeners() {
255
+ _detachInteractionListeners();
256
+
257
+ const handler = () => {
258
+ _detachInteractionListeners();
259
+ _onFirstInteraction();
260
+ };
261
+ _currentInteractionHandler = handler;
262
+ window.addEventListener("mousemove", handler, { passive: true });
263
+ window.addEventListener("touchstart", handler, { passive: true });
264
+ window.addEventListener("keydown", handler, { passive: true });
265
+ }
266
+
267
+ _attachInteractionListeners();
268
+
269
+ async function _beginSpeculativeSolve() {
270
+ if (speculative.state !== "waiting") return;
271
+ speculative.state = "fetching";
272
+
273
+ const widget = document.querySelector("cap-widget");
274
+ if (!widget) {
275
+ speculative.state = "idle";
276
+ return;
277
+ }
278
+
279
+ let apiEndpoint = widget.getAttribute("data-cap-api-endpoint");
280
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
281
+ apiEndpoint = "/";
282
+ }
283
+ if (!apiEndpoint) {
284
+ speculative.state = "idle";
285
+ return;
286
+ }
287
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
288
+
289
+ try {
290
+ const raw = await capFetch(`${apiEndpoint}challenge`, { method: "POST" });
291
+ let resp;
292
+ try {
293
+ resp = await raw.json();
294
+ } catch {
295
+ throw new Error("Failed to parse speculative challenge response");
296
+ }
297
+ if (resp.error) throw new Error(resp.error);
298
+
299
+ resp._apiEndpoint = apiEndpoint;
300
+ speculative.challengeResp = resp;
301
+
302
+ const { challenge, token } = resp;
303
+ let challenges = challenge;
304
+ if (!Array.isArray(challenges)) {
305
+ let i = 0;
306
+ challenges = Array.from({ length: challenge.c }, () => {
307
+ i++;
308
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
309
+ });
310
+ }
311
+ speculative.challenges = challenges;
312
+ speculative.state = "solving";
313
+
314
+ speculative.solvePromise = _speculativeSolveAll(challenges);
315
+ } catch (e) {
316
+ console.warn("[cap] speculative challenge fetch failed:", e);
317
+ speculative.state = "error";
318
+ speculative.notify();
319
+ }
320
+ }
321
+
322
+ async function _speculativeSolveAll(challenges) {
323
+ _getSharedWorkerUrl();
324
+
325
+ let wasmModule = null;
326
+ try {
327
+ wasmModule = await getWasmModule();
328
+ } catch {}
329
+
330
+ _speculativePool.setWasm(wasmModule);
331
+
332
+ const total = challenges.length;
333
+ const results = new Array(total);
334
+
335
+ let concurrency = SPECULATIVE_WORKERS;
336
+ let promoted = false;
337
+
338
+ speculative.promoteFn = (fullCount) => {
339
+ if (promoted) return;
340
+ promoted = true;
341
+ concurrency = fullCount;
342
+ _speculativePool._size = fullCount;
343
+ _speculativePool._ensureSize(fullCount);
344
+ };
345
+
346
+ if (speculative.pendingPromotion !== null) {
347
+ speculative.promoteFn(speculative.pendingPromotion);
348
+ speculative.pendingPromotion = null;
349
+ }
350
+
351
+ let nextIndex = 0;
352
+
353
+ while (nextIndex < total) {
354
+ const batchSize = concurrency;
355
+ const batch = [];
356
+ const batchIndices = [];
357
+
358
+ for (let i = 0; i < batchSize && nextIndex < total; i++) {
359
+ batchIndices.push(nextIndex);
360
+ batch.push(challenges[nextIndex]);
361
+ nextIndex++;
362
+ }
363
+
364
+ _speculativePool._ensureSize(Math.max(concurrency, batchSize));
365
+
366
+ const batchResults = await Promise.all(
367
+ batch.map((challenge) =>
368
+ _speculativePool.run(challenge[0], challenge[1]).then((nonce) => {
369
+ speculative.completedCount++;
370
+ return nonce;
371
+ }),
372
+ ),
373
+ );
374
+
375
+ for (let i = 0; i < batchIndices.length; i++) {
376
+ results[batchIndices[i]] = batchResults[i];
377
+ }
378
+
379
+ if (!promoted && nextIndex < total) {
380
+ await new Promise((resolve) => setTimeout(resolve, SPECULATIVE_YIELD_MS));
381
+ }
382
+ }
383
+
384
+ speculative.results = results;
385
+ speculative.state = "redeeming";
386
+ _speculativeRedeem(results);
387
+ return results;
388
+ }
389
+
390
+ async function _speculativeRedeem(solutions) {
391
+ try {
392
+ const challengeResp = speculative.challengeResp;
393
+ const apiEndpoint = challengeResp._apiEndpoint;
394
+ if (!apiEndpoint) throw new Error("[cap] speculative redeem: missing apiEndpoint");
395
+
396
+ let instrOut = null;
397
+ if (challengeResp.instrumentation) {
398
+ instrOut = await runInstrumentationChallenge(challengeResp.instrumentation);
399
+ if (instrOut?.__timeout || instrOut?.__blocked) {
400
+ speculative.state = "done";
401
+ speculative.notify();
402
+ return;
403
+ }
404
+ }
405
+
406
+ const redeemRaw = await capFetch(`${apiEndpoint}redeem`, {
407
+ method: "POST",
408
+ body: JSON.stringify({
409
+ token: challengeResp.token,
410
+ solutions,
411
+ ...(instrOut && { instr: instrOut }),
412
+ }),
413
+ headers: { "Content-Type": "application/json" },
414
+ });
415
+
416
+ let resp;
417
+ try {
418
+ resp = await redeemRaw.json();
419
+ } catch {
420
+ throw new Error("Failed to parse speculative redeem response");
421
+ }
422
+
423
+ if (!resp.success) throw new Error(resp.error || "Speculative redeem failed");
424
+
425
+ speculative.token = resp.token;
426
+ speculative.tokenExpires = new Date(resp.expires).getTime();
427
+ speculative.state = "done";
428
+ speculative.notify();
429
+ } catch (e) {
430
+ console.warn("[cap] speculative redeem failed (will redo on click):", e);
431
+ speculative.state = "done";
432
+ speculative.notify();
433
+ }
434
+ }
435
+
436
+ let _sharedWorkerUrl = null;
437
+
438
+ function _getSharedWorkerUrl() {
439
+ if (_sharedWorkerUrl) return _sharedWorkerUrl;
440
+
441
+ _sharedWorkerUrl = URL.createObjectURL(
442
+ new Blob([`%%workerScript%%`], { type: "application/javascript" }),
443
+ );
444
+ return _sharedWorkerUrl;
445
+ }
446
+
447
+ class WorkerPool {
448
+ constructor(size) {
449
+ this._size = size;
450
+ this._workers = [];
451
+ this._idle = [];
452
+ this._queue = [];
453
+ this._wasmModule = null;
454
+ this._spawnFailures = 0;
455
+ }
456
+
457
+ setWasm(wasmModule) {
458
+ this._wasmModule = wasmModule;
459
+ }
460
+
461
+ _spawn() {
462
+ const url = _getSharedWorkerUrl();
463
+ const w = new Worker(url);
464
+ w._busy = false;
465
+ this._workers.push(w);
466
+ this._idle.push(w);
467
+ return w;
468
+ }
469
+
470
+ _replaceWorker(deadWorker) {
471
+ const idx = this._workers.indexOf(deadWorker);
472
+ if (idx !== -1) this._workers.splice(idx, 1);
473
+ const idleIdx = this._idle.indexOf(deadWorker);
474
+ if (idleIdx !== -1) this._idle.splice(idleIdx, 1);
475
+
476
+ try {
477
+ deadWorker.terminate();
478
+ } catch {}
479
+
480
+ this._spawnFailures++;
481
+ if (this._spawnFailures > 3) {
482
+ console.error("[cap] worker spawn failed repeatedly, not retrying");
483
+ return null;
484
+ }
485
+
486
+ return this._spawn();
487
+ }
488
+
489
+ _ensureSize(n) {
490
+ while (this._workers.length < n) this._spawn();
491
+ }
492
+
493
+ run(salt, target) {
494
+ return new Promise((resolve, reject) => {
495
+ this._queue.push({ salt, target, resolve, reject });
496
+ this._dispatch();
497
+ });
498
+ }
499
+
500
+ _dispatch() {
501
+ while (this._idle.length > 0 && this._queue.length > 0) {
502
+ const worker = this._idle.shift();
503
+ const { salt, target, resolve, reject } = this._queue.shift();
504
+
505
+ let settled = false;
506
+
507
+ const onMessage = ({ data }) => {
508
+ if (settled) return;
509
+ settled = true;
510
+ worker.removeEventListener("message", onMessage);
511
+ worker.removeEventListener("error", onError);
512
+ this._spawnFailures = 0;
513
+ this._idle.push(worker);
514
+ if (!data.found) {
515
+ reject(new Error(data.error || "worker failed"));
516
+ } else {
517
+ resolve(data.nonce);
518
+ }
519
+ this._dispatch();
520
+ };
521
+
522
+ const onError = (err) => {
523
+ if (settled) return;
524
+ settled = true;
525
+ worker.removeEventListener("message", onMessage);
526
+ worker.removeEventListener("error", onError);
527
+ const replacement = this._replaceWorker(worker);
528
+ reject(err);
529
+ if (replacement) {
530
+ this._dispatch();
531
+ }
532
+ };
533
+
534
+ worker.addEventListener("message", onMessage);
535
+ worker.addEventListener("error", onError);
536
+
537
+ if (this._wasmModule) {
538
+ worker.postMessage({ salt, target, wasmModule: this._wasmModule }, []);
539
+ } else {
540
+ worker.postMessage({ salt, target });
541
+ }
542
+ }
543
+ }
544
+
545
+ terminate() {
546
+ for (const w of this._workers) {
547
+ try {
548
+ w.terminate();
549
+ } catch {}
550
+ }
551
+ this._workers = [];
552
+ this._idle = [];
553
+ this._queue = [];
554
+ }
555
+ }
556
+
557
+ const _speculativePool = new WorkerPool(1);
558
+ _speculativePool._spawn();
559
+
182
560
  class CapWidget extends HTMLElement {
183
- #workerUrl = "";
184
561
  #resetTimer = null;
185
562
  #workersCount = navigator.hardwareConcurrency || 8;
186
563
  token = null;
@@ -221,13 +598,7 @@
221
598
  }
222
599
 
223
600
  initialize() {
224
- this.#workerUrl = URL.createObjectURL(
225
- // this placeholder will be replaced with the actual worker by the build script
226
-
227
- new Blob([`%%workerScript%%`], {
228
- type: "application/javascript",
229
- }),
230
- );
601
+ _getSharedWorkerUrl();
231
602
  }
232
603
 
233
604
  attributeChangedCallback(name, _, value) {
@@ -277,6 +648,15 @@
277
648
  this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
278
649
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
279
650
  this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
651
+
652
+ if (speculative.state === "idle" || speculative.state === "waiting") {
653
+ if (_speculativeTimer) {
654
+ clearTimeout(_speculativeTimer);
655
+ _speculativeTimer = null;
656
+ }
657
+ speculative.state = "waiting";
658
+ _speculativeTimer = setTimeout(() => _beginSpeculativeSolve(), SPECULATIVE_DELAY_MS);
659
+ }
280
660
  }
281
661
 
282
662
  async solve() {
@@ -287,106 +667,173 @@
287
667
  try {
288
668
  this.#solving = true;
289
669
  this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
290
-
291
670
  this.#div.setAttribute(
292
671
  "aria-label",
293
672
  this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"),
294
673
  );
295
-
296
674
  this.dispatchEvent("progress", { progress: 0 });
297
675
 
298
676
  try {
299
677
  let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
300
-
301
678
  if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
302
679
  apiEndpoint = "/";
303
- } else if (!apiEndpoint)
680
+ } else if (!apiEndpoint) {
304
681
  throw new Error(
305
682
  "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
306
683
  );
307
-
308
- if (!apiEndpoint.endsWith("/")) {
309
- apiEndpoint += "/";
310
684
  }
685
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
311
686
 
312
- const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
313
- method: "POST",
314
- });
315
-
687
+ let solutions;
316
688
  let challengeResp;
317
- try {
318
- challengeResp = await challengeRaw.json();
319
- } catch (parseErr) {
320
- throw new Error("Failed to parse challenge response from server");
321
- }
322
689
 
323
- if (challengeResp.error) {
324
- throw new Error(challengeResp.error);
690
+ if (
691
+ speculative.state === "done" &&
692
+ speculative.token &&
693
+ speculative.tokenExpires &&
694
+ Date.now() < speculative.tokenExpires
695
+ ) {
696
+ this.dispatchEvent("progress", { progress: 100 });
697
+
698
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
699
+ if (this.querySelector(`input[name='${fieldName}']`)) {
700
+ this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
701
+ }
702
+ this.dispatchEvent("solve", { token: speculative.token });
703
+ this.token = speculative.token;
704
+
705
+ const expiresIn = speculative.tokenExpires - Date.now();
706
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
707
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
708
+
709
+ this.#div.setAttribute(
710
+ "aria-label",
711
+ this.getI18nText(
712
+ "verified-aria-label",
713
+ "We have verified you're a human, you may now continue",
714
+ ),
715
+ );
716
+
717
+ _resetSpeculativeState();
718
+ this.#solving = false;
719
+ return;
325
720
  }
326
721
 
327
- const { challenge, token } = challengeResp;
722
+ if (speculative.state === "done") {
723
+ solutions = speculative.results;
724
+ challengeResp = speculative.challengeResp;
725
+ this.dispatchEvent("progress", { progress: 100 });
726
+ } else if (
727
+ speculative.state === "solving" ||
728
+ speculative.state === "redeeming" ||
729
+ speculative.state === "fetching" ||
730
+ speculative.state === "waiting"
731
+ ) {
732
+ if (speculative.state === "waiting") {
733
+ if (_speculativeTimer) {
734
+ clearTimeout(_speculativeTimer);
735
+ _speculativeTimer = null;
736
+ }
737
+ speculative.state = "waiting";
738
+ _beginSpeculativeSolve();
739
+ }
328
740
 
329
- let challenges = challenge;
741
+ speculative.pendingPromotion = this.#workersCount;
742
+ if (speculative.promoteFn) {
743
+ speculative.promoteFn(this.#workersCount);
744
+ }
330
745
 
331
- if (!Array.isArray(challenges)) {
332
- let i = 0;
746
+ const progressInterval = setInterval(() => {
747
+ if (speculative.state !== "solving" && speculative.state !== "redeeming") {
748
+ clearInterval(progressInterval);
749
+ return;
750
+ }
751
+ const total = speculative.challenges ? speculative.challenges.length : 1;
752
+ const done = speculative.completedCount;
753
+ const visual =
754
+ speculative.state === "redeeming"
755
+ ? 99
756
+ : Math.min(98, Math.round((done / total) * 100));
757
+ this.dispatchEvent("progress", { progress: visual });
758
+ }, 150);
759
+
760
+ await new Promise((resolve) => speculative.onSettled(resolve));
761
+ clearInterval(progressInterval);
762
+
763
+ if (speculative.state !== "done") {
764
+ throw new Error("Speculative solve failed – please try again");
765
+ }
333
766
 
334
- challenges = Array.from({ length: challenge.c }, () => {
335
- i = i + 1;
767
+ if (
768
+ speculative.token &&
769
+ speculative.tokenExpires &&
770
+ Date.now() < speculative.tokenExpires
771
+ ) {
772
+ this.dispatchEvent("progress", { progress: 100 });
773
+
774
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
775
+ if (this.querySelector(`input[name='${fieldName}']`)) {
776
+ this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
777
+ }
778
+ this.dispatchEvent("solve", { token: speculative.token });
779
+ this.token = speculative.token;
780
+
781
+ const expiresIn = speculative.tokenExpires - Date.now();
782
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
783
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
784
+
785
+ this.#div.setAttribute(
786
+ "aria-label",
787
+ this.getI18nText(
788
+ "verified-aria-label",
789
+ "We have verified you're a human, you may now continue",
790
+ ),
791
+ );
792
+
793
+ _resetSpeculativeState();
794
+ this.#solving = false;
795
+ return;
796
+ }
336
797
 
337
- return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
798
+ solutions = speculative.results;
799
+ challengeResp = speculative.challengeResp;
800
+ this.dispatchEvent("progress", { progress: 100 });
801
+ } else {
802
+ const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
803
+ method: "POST",
338
804
  });
805
+ try {
806
+ challengeResp = await challengeRaw.json();
807
+ } catch {
808
+ throw new Error("Failed to parse challenge response from server");
809
+ }
810
+ if (challengeResp.error) throw new Error(challengeResp.error);
811
+
812
+ const { challenge, token } = challengeResp;
813
+ let challenges = challenge;
814
+ if (!Array.isArray(challenges)) {
815
+ let i = 0;
816
+ challenges = Array.from({ length: challenge.c }, () => {
817
+ i++;
818
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
819
+ });
820
+ }
821
+
822
+ solutions = await this.solveChallenges(challenges);
339
823
  }
340
824
 
341
825
  const instrPromise = challengeResp.instrumentation
342
826
  ? runInstrumentationChallenge(challengeResp.instrumentation)
343
827
  : Promise.resolve(null);
344
828
 
345
- const powPromise = this.solveChallenges(challenges);
346
-
347
- const instrErrorPromise = instrPromise.then((result) => {
348
- if (result && result.__timeout) return result;
349
- return null;
350
- });
351
-
352
- const instrEarlyError = await Promise.race([
353
- instrErrorPromise,
354
- powPromise.then(() => null),
355
- ]);
356
-
357
- if (instrEarlyError && instrEarlyError.__timeout) {
358
- const errMsg = "Instrumentation timeout — please try again later";
359
- this.updateUIBlocked(this.getI18nText("error-label", "Error"), true);
360
- this.#div.setAttribute(
361
- "aria-label",
362
- this.getI18nText("error-aria-label", "An error occurred, please try again"),
363
- );
364
- this.removeEventListener("error", this.boundHandleError);
365
- const errEvent = new CustomEvent("error", {
366
- bubbles: true,
367
- composed: true,
368
- detail: { isCap: true, message: errMsg },
369
- });
370
- super.dispatchEvent(errEvent);
371
- this.addEventListener("error", this.boundHandleError);
372
- this.executeAttributeCode("onerror", errEvent);
373
- console.error("[cap]", errMsg);
374
- this.#solving = false;
375
- return;
376
- }
377
-
378
- const [solutions, instrOut] = await Promise.all([powPromise, instrPromise]);
829
+ const instrOut = await instrPromise;
379
830
 
380
831
  if (instrOut?.__timeout || instrOut?.__blocked) {
381
- this.updateUIBlocked(
382
- this.getI18nText("error-label", "Error"),
383
- instrOut && instrOut.__blocked,
384
- );
832
+ this.updateUIBlocked(this.getI18nText("error-label", "Error"), instrOut?.__blocked);
385
833
  this.#div.setAttribute(
386
834
  "aria-label",
387
835
  this.getI18nText("error-aria-label", "An error occurred, please try again"),
388
836
  );
389
-
390
837
  this.removeEventListener("error", this.boundHandleError);
391
838
  const errEvent = new CustomEvent("error", {
392
839
  bubbles: true,
@@ -395,13 +842,14 @@
395
842
  });
396
843
  super.dispatchEvent(errEvent);
397
844
  this.addEventListener("error", this.boundHandleError);
398
-
399
845
  this.executeAttributeCode("onerror", errEvent);
400
846
  console.error("[cap]", "Instrumentation failed");
401
847
  this.#solving = false;
402
848
  return;
403
849
  }
404
850
 
851
+ const { token } = challengeResp;
852
+
405
853
  const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
406
854
  method: "POST",
407
855
  body: JSON.stringify({
@@ -421,6 +869,7 @@
421
869
 
422
870
  this.dispatchEvent("progress", { progress: 100 });
423
871
  if (!resp.success) throw new Error(resp.error || "Invalid solution");
872
+
424
873
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
425
874
  if (this.querySelector(`input[name='${fieldName}']`)) {
426
875
  this.querySelector(`input[name='${fieldName}']`).value = resp.token;
@@ -429,6 +878,8 @@
429
878
  this.dispatchEvent("solve", { token: resp.token });
430
879
  this.token = resp.token;
431
880
 
881
+ _resetSpeculativeState();
882
+
432
883
  if (this.#resetTimer) clearTimeout(this.#resetTimer);
433
884
  const expiresIn = new Date(resp.expires).getTime() - Date.now();
434
885
  if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
@@ -463,6 +914,9 @@
463
914
  const total = challenges.length;
464
915
  let completed = 0;
465
916
 
917
+ const speculativeHead = 0;
918
+ const remaining = total - speculativeHead;
919
+
466
920
  let wasmModule = null;
467
921
  const wasmSupported =
468
922
  typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
@@ -492,67 +946,31 @@
492
946
  }
493
947
  }
494
948
 
495
- const workers = Array(this.#workersCount)
496
- .fill(null)
497
- .map(() => {
498
- try {
499
- return new Worker(this.#workerUrl);
500
- } catch (error) {
501
- console.error("[cap] Failed to create worker:", error);
502
- throw new Error("Worker creation failed");
503
- }
504
- });
505
-
506
- const solveSingleChallenge = ([salt, target], workerId) =>
507
- new Promise((resolve, reject) => {
508
- const worker = workers[workerId];
509
- if (!worker) {
510
- reject(new Error("Worker not available"));
511
- return;
512
- }
513
-
514
- worker.onmessage = ({ data }) => {
515
- if (!data.found) return;
516
-
517
- completed++;
518
- this.dispatchEvent("progress", {
519
- progress: Math.round((completed / total) * 100),
520
- });
521
-
522
- resolve(data.nonce);
523
- };
524
-
525
- worker.onerror = (err) => {
526
- this.error(`Error in worker: ${err.message || err}`);
527
- reject(err);
528
- };
529
-
530
- if (wasmModule) {
531
- worker.postMessage({ salt, target, wasmModule }, []);
532
- } else {
533
- worker.postMessage({ salt, target });
534
- }
535
- });
949
+ const pool = new WorkerPool(this.#workersCount);
950
+ pool.setWasm(wasmModule);
951
+ pool._ensureSize(this.#workersCount);
536
952
 
537
953
  const results = [];
538
954
  try {
539
955
  for (let i = 0; i < challenges.length; i += this.#workersCount) {
540
956
  const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length));
541
957
  const chunkResults = await Promise.all(
542
- chunk.map((c, idx) => solveSingleChallenge(c, idx)),
958
+ chunk.map(([salt, target]) =>
959
+ pool.run(salt, target).then((nonce) => {
960
+ completed++;
961
+ const visual = Math.min(
962
+ 99,
963
+ Math.round(((speculativeHead + completed) / total) * 100),
964
+ );
965
+ this.dispatchEvent("progress", { progress: visual });
966
+ return nonce;
967
+ }),
968
+ ),
543
969
  );
544
970
  results.push(...chunkResults);
545
971
  }
546
972
  } finally {
547
- workers.forEach((w) => {
548
- if (w) {
549
- try {
550
- w.terminate();
551
- } catch (error) {
552
- console.error("[cap] error terminating worker:", error);
553
- }
554
- }
555
- });
973
+ pool.terminate();
556
974
  }
557
975
 
558
976
  return results;
@@ -645,7 +1063,9 @@
645
1063
  if (current) {
646
1064
  current.classList.remove("active");
647
1065
  current.classList.add("exit");
648
- current.addEventListener("transitionend", () => current.remove(), { once: true });
1066
+ current.addEventListener("transitionend", () => current.remove(), {
1067
+ once: true,
1068
+ });
649
1069
  }
650
1070
  }
651
1071
 
@@ -689,11 +1109,14 @@
689
1109
  if (current) {
690
1110
  current.classList.remove("active");
691
1111
  current.classList.add("exit");
692
- current.addEventListener("transitionend", () => current.remove(), { once: true });
1112
+ current.addEventListener("transitionend", () => current.remove(), {
1113
+ once: true,
1114
+ });
693
1115
  }
694
1116
 
695
1117
  const link = next.querySelector(".cap-troubleshoot-link");
696
1118
  if (link) {
1119
+ console.log("linkblud")
697
1120
  link.addEventListener("click", (e) => {
698
1121
  e.stopPropagation();
699
1122
  });
@@ -742,8 +1165,10 @@
742
1165
  if (!code) {
743
1166
  return;
744
1167
  }
745
-
746
- console.error("[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.");
1168
+
1169
+ console.error(
1170
+ "[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.",
1171
+ );
747
1172
 
748
1173
  new Function("event", code).call(this, event);
749
1174
  }
@@ -803,11 +1228,6 @@
803
1228
  clearTimeout(this.#resetTimer);
804
1229
  this.#resetTimer = null;
805
1230
  }
806
-
807
- if (this.#workerUrl) {
808
- URL.revokeObjectURL(this.#workerUrl);
809
- this.#workerUrl = "";
810
- }
811
1231
  }
812
1232
  }
813
1233