@cap.js/widget 0.1.37 → 0.1.39

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
@@ -1,24 +1,13 @@
1
1
  (() => {
2
2
  const WASM_VERSION = "0.0.6";
3
+ const hasHaptics =
4
+ "vibrate" in navigator && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3
5
 
4
6
  if (typeof window === "undefined") {
5
7
  return;
6
8
  }
7
9
 
8
- const capFetch = (u, _conf = {}) => {
9
- const conf = {
10
- ..._conf,
11
- headers: {
12
- ...(_conf.headers || {}),
13
- "Cap-Stamp": btoa(
14
- String.fromCharCode(
15
- ...[[Date.now()]].map((n) => [n >> 24, n >> 16, n >> 8, n[0]].map((x) => x & 255))[0],
16
- ),
17
- ).replace(/=/g, ""),
18
- "Cap-Solver": `0,${WASM_VERSION}`,
19
- },
20
- };
21
-
10
+ const capFetch = (u, conf = {}) => {
22
11
  if (window?.CAP_CUSTOM_FETCH) {
23
12
  return window.CAP_CUSTOM_FETCH(u, conf);
24
13
  }
@@ -64,7 +53,7 @@
64
53
 
65
54
  var compressed = b64ToUint8(instrBytes);
66
55
 
67
- const scriptText = await new Promise(function (resolve, reject) {
56
+ const scriptText = await new Promise((resolve, reject) => {
68
57
  try {
69
58
  var ds = new DecompressionStream("deflate-raw");
70
59
  var writer = ds.writable.getWriter();
@@ -89,7 +78,7 @@
89
78
  reader.read().then(pump).catch(reject);
90
79
  writer
91
80
  .write(compressed)
92
- .then(function () {
81
+ .then(() => {
93
82
  writer.close();
94
83
  })
95
84
  .catch(reject);
@@ -98,8 +87,8 @@
98
87
  }
99
88
  });
100
89
 
101
- return new Promise(function (resolve) {
102
- var timeout = setTimeout(function () {
90
+ return new Promise((resolve) => {
91
+ var timeout = setTimeout(() => {
103
92
  cleanup();
104
93
  resolve({ __timeout: true });
105
94
  }, 20000);
@@ -125,7 +114,10 @@
125
114
  if (d.type === "cap:instr") {
126
115
  cleanup();
127
116
  if (d.blocked) {
128
- resolve({ __blocked: true, blockReason: d.blockReason || "automated_browser" });
117
+ resolve({
118
+ __blocked: true,
119
+ blockReason: d.blockReason || "automated_browser",
120
+ });
129
121
  } else if (d.result) {
130
122
  resolve(d.result);
131
123
  } else {
@@ -179,8 +171,382 @@
179
171
  const prefersReducedMotion = () =>
180
172
  window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
181
173
 
174
+ const SPECULATIVE_DELAY_MS = 2500;
175
+ const SPECULATIVE_WORKERS = 1;
176
+ const SPECULATIVE_YIELD_MS = 120;
177
+
178
+ const speculative = {
179
+ state: "idle",
180
+ challengeResp: null,
181
+ challenges: null,
182
+ results: [],
183
+ completedCount: 0,
184
+ solvePromise: null,
185
+ promoteFn: null,
186
+ _listeners: [],
187
+ pendingPromotion: null,
188
+ token: null,
189
+ tokenExpires: null,
190
+
191
+ notify() {
192
+ for (const fn of this._listeners) fn();
193
+ this._listeners = [];
194
+ },
195
+
196
+ onSettled(fn) {
197
+ if (this.state === "done" || this.state === "error") {
198
+ fn();
199
+ } else {
200
+ this._listeners.push(fn);
201
+ }
202
+ },
203
+ };
204
+
205
+ function _resetSpeculativeState() {
206
+ speculative.state = "idle";
207
+ speculative.challengeResp = null;
208
+ speculative.challenges = null;
209
+ speculative.results = [];
210
+ speculative.completedCount = 0;
211
+ speculative.solvePromise = null;
212
+ speculative.promoteFn = null;
213
+ speculative.pendingPromotion = null;
214
+ speculative._listeners = [];
215
+ speculative.token = null;
216
+ speculative.tokenExpires = null;
217
+
218
+ _attachInteractionListeners();
219
+ }
220
+
221
+ let _speculativeTimer = null;
222
+
223
+ function _onFirstInteraction() {
224
+ if (speculative.state !== "idle") return;
225
+ speculative.state = "waiting";
226
+
227
+ _speculativeTimer = setTimeout(() => {
228
+ _beginSpeculativeSolve();
229
+ }, SPECULATIVE_DELAY_MS);
230
+ }
231
+
232
+ let _currentInteractionHandler = null;
233
+
234
+ function _detachInteractionListeners() {
235
+ if (_currentInteractionHandler) {
236
+ window.removeEventListener("mousemove", _currentInteractionHandler);
237
+ window.removeEventListener("touchstart", _currentInteractionHandler);
238
+ window.removeEventListener("keydown", _currentInteractionHandler);
239
+ _currentInteractionHandler = null;
240
+ }
241
+ }
242
+
243
+ function _attachInteractionListeners() {
244
+ _detachInteractionListeners();
245
+
246
+ const handler = () => {
247
+ _detachInteractionListeners();
248
+ _onFirstInteraction();
249
+ };
250
+ _currentInteractionHandler = handler;
251
+ window.addEventListener("mousemove", handler, { passive: true });
252
+ window.addEventListener("touchstart", handler, { passive: true });
253
+ window.addEventListener("keydown", handler, { passive: true });
254
+ }
255
+
256
+ _attachInteractionListeners();
257
+
258
+ async function _beginSpeculativeSolve() {
259
+ if (speculative.state !== "waiting") return;
260
+ speculative.state = "fetching";
261
+
262
+ const widget = document.querySelector("cap-widget");
263
+ if (!widget) {
264
+ speculative.state = "idle";
265
+ return;
266
+ }
267
+
268
+ let apiEndpoint = widget.getAttribute("data-cap-api-endpoint");
269
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
270
+ apiEndpoint = "/";
271
+ }
272
+ if (!apiEndpoint) {
273
+ speculative.state = "idle";
274
+ return;
275
+ }
276
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
277
+
278
+ try {
279
+ const raw = await capFetch(`${apiEndpoint}challenge`, { method: "POST" });
280
+ let resp;
281
+ try {
282
+ resp = await raw.json();
283
+ } catch {
284
+ throw new Error("Failed to parse speculative challenge response");
285
+ }
286
+ if (resp.error) throw new Error(resp.error);
287
+
288
+ resp._apiEndpoint = apiEndpoint;
289
+ speculative.challengeResp = resp;
290
+
291
+ const { challenge, token } = resp;
292
+ let challenges = challenge;
293
+ if (!Array.isArray(challenges)) {
294
+ let i = 0;
295
+ challenges = Array.from({ length: challenge.c }, () => {
296
+ i++;
297
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
298
+ });
299
+ }
300
+ speculative.challenges = challenges;
301
+ speculative.state = "solving";
302
+
303
+ speculative.solvePromise = _speculativeSolveAll(challenges);
304
+ } catch (e) {
305
+ console.warn("[cap] speculative challenge fetch failed:", e);
306
+ speculative.state = "error";
307
+ speculative.notify();
308
+ }
309
+ }
310
+
311
+ async function _speculativeSolveAll(challenges) {
312
+ _getSharedWorkerUrl();
313
+
314
+ let wasmModule = null;
315
+ try {
316
+ wasmModule = await getWasmModule();
317
+ } catch {}
318
+
319
+ _speculativePool.setWasm(wasmModule);
320
+
321
+ const total = challenges.length;
322
+ const results = new Array(total);
323
+
324
+ let concurrency = SPECULATIVE_WORKERS;
325
+ let promoted = false;
326
+
327
+ speculative.promoteFn = (fullCount) => {
328
+ if (promoted) return;
329
+ promoted = true;
330
+ concurrency = fullCount;
331
+ _speculativePool._size = fullCount;
332
+ _speculativePool._ensureSize(fullCount);
333
+ };
334
+
335
+ if (speculative.pendingPromotion !== null) {
336
+ speculative.promoteFn(speculative.pendingPromotion);
337
+ speculative.pendingPromotion = null;
338
+ }
339
+
340
+ let nextIndex = 0;
341
+
342
+ while (nextIndex < total) {
343
+ const batchSize = concurrency;
344
+ const batch = [];
345
+ const batchIndices = [];
346
+
347
+ for (let i = 0; i < batchSize && nextIndex < total; i++) {
348
+ batchIndices.push(nextIndex);
349
+ batch.push(challenges[nextIndex]);
350
+ nextIndex++;
351
+ }
352
+
353
+ _speculativePool._ensureSize(Math.max(concurrency, batchSize));
354
+
355
+ const batchResults = await Promise.all(
356
+ batch.map((challenge) =>
357
+ _speculativePool.run(challenge[0], challenge[1]).then((nonce) => {
358
+ speculative.completedCount++;
359
+ return nonce;
360
+ }),
361
+ ),
362
+ );
363
+
364
+ for (let i = 0; i < batchIndices.length; i++) {
365
+ results[batchIndices[i]] = batchResults[i];
366
+ }
367
+
368
+ if (!promoted && nextIndex < total) {
369
+ await new Promise((resolve) => setTimeout(resolve, SPECULATIVE_YIELD_MS));
370
+ }
371
+ }
372
+
373
+ speculative.results = results;
374
+ speculative.state = "redeeming";
375
+ _speculativeRedeem(results);
376
+ return results;
377
+ }
378
+
379
+ async function _speculativeRedeem(solutions) {
380
+ try {
381
+ const challengeResp = speculative.challengeResp;
382
+ const apiEndpoint = challengeResp._apiEndpoint;
383
+ if (!apiEndpoint) throw new Error("[cap] speculative redeem: missing apiEndpoint");
384
+
385
+ let instrOut = null;
386
+ if (challengeResp.instrumentation) {
387
+ instrOut = await runInstrumentationChallenge(challengeResp.instrumentation);
388
+ if (instrOut?.__timeout || instrOut?.__blocked) {
389
+ speculative.state = "done";
390
+ speculative.notify();
391
+ return;
392
+ }
393
+ }
394
+
395
+ const redeemRaw = await capFetch(`${apiEndpoint}redeem`, {
396
+ method: "POST",
397
+ body: JSON.stringify({
398
+ token: challengeResp.token,
399
+ solutions,
400
+ ...(instrOut && { instr: instrOut }),
401
+ }),
402
+ headers: { "Content-Type": "application/json" },
403
+ });
404
+
405
+ let resp;
406
+ try {
407
+ resp = await redeemRaw.json();
408
+ } catch {
409
+ throw new Error("Failed to parse speculative redeem response");
410
+ }
411
+
412
+ if (!resp.success) throw new Error(resp.error || "Speculative redeem failed");
413
+
414
+ speculative.token = resp.token;
415
+ speculative.tokenExpires = new Date(resp.expires).getTime();
416
+ speculative.state = "done";
417
+ speculative.notify();
418
+ } catch (e) {
419
+ console.warn("[cap] speculative redeem failed (will redo on click):", e);
420
+ speculative.state = "done";
421
+ speculative.notify();
422
+ }
423
+ }
424
+
425
+ let _sharedWorkerUrl = null;
426
+
427
+ function _getSharedWorkerUrl() {
428
+ if (_sharedWorkerUrl) return _sharedWorkerUrl;
429
+
430
+ _sharedWorkerUrl = URL.createObjectURL(
431
+ new Blob([`%%workerScript%%`], { type: "application/javascript" }),
432
+ );
433
+ return _sharedWorkerUrl;
434
+ }
435
+
436
+ class WorkerPool {
437
+ constructor(size) {
438
+ this._size = size;
439
+ this._workers = [];
440
+ this._idle = [];
441
+ this._queue = [];
442
+ this._wasmModule = null;
443
+ this._spawnFailures = 0;
444
+ }
445
+
446
+ setWasm(wasmModule) {
447
+ this._wasmModule = wasmModule;
448
+ }
449
+
450
+ _spawn() {
451
+ const url = _getSharedWorkerUrl();
452
+ const w = new Worker(url);
453
+ w._busy = false;
454
+ this._workers.push(w);
455
+ this._idle.push(w);
456
+ return w;
457
+ }
458
+
459
+ _replaceWorker(deadWorker) {
460
+ const idx = this._workers.indexOf(deadWorker);
461
+ if (idx !== -1) this._workers.splice(idx, 1);
462
+ const idleIdx = this._idle.indexOf(deadWorker);
463
+ if (idleIdx !== -1) this._idle.splice(idleIdx, 1);
464
+
465
+ try {
466
+ deadWorker.terminate();
467
+ } catch {}
468
+
469
+ this._spawnFailures++;
470
+ if (this._spawnFailures > 3) {
471
+ console.error("[cap] worker spawn failed repeatedly, not retrying");
472
+ return null;
473
+ }
474
+
475
+ return this._spawn();
476
+ }
477
+
478
+ _ensureSize(n) {
479
+ while (this._workers.length < n) this._spawn();
480
+ }
481
+
482
+ run(salt, target) {
483
+ return new Promise((resolve, reject) => {
484
+ this._queue.push({ salt, target, resolve, reject });
485
+ this._dispatch();
486
+ });
487
+ }
488
+
489
+ _dispatch() {
490
+ while (this._idle.length > 0 && this._queue.length > 0) {
491
+ const worker = this._idle.shift();
492
+ const { salt, target, resolve, reject } = this._queue.shift();
493
+
494
+ let settled = false;
495
+
496
+ const onMessage = ({ data }) => {
497
+ if (settled) return;
498
+ settled = true;
499
+ worker.removeEventListener("message", onMessage);
500
+ worker.removeEventListener("error", onError);
501
+ this._spawnFailures = 0;
502
+ this._idle.push(worker);
503
+ if (!data.found) {
504
+ reject(new Error(data.error || "worker failed"));
505
+ } else {
506
+ resolve(data.nonce);
507
+ }
508
+ this._dispatch();
509
+ };
510
+
511
+ const onError = (err) => {
512
+ if (settled) return;
513
+ settled = true;
514
+ worker.removeEventListener("message", onMessage);
515
+ worker.removeEventListener("error", onError);
516
+ const replacement = this._replaceWorker(worker);
517
+ reject(err);
518
+ if (replacement) {
519
+ this._dispatch();
520
+ }
521
+ };
522
+
523
+ worker.addEventListener("message", onMessage);
524
+ worker.addEventListener("error", onError);
525
+
526
+ if (this._wasmModule) {
527
+ worker.postMessage({ salt, target, wasmModule: this._wasmModule }, []);
528
+ } else {
529
+ worker.postMessage({ salt, target });
530
+ }
531
+ }
532
+ }
533
+
534
+ terminate() {
535
+ for (const w of this._workers) {
536
+ try {
537
+ w.terminate();
538
+ } catch {}
539
+ }
540
+ this._workers = [];
541
+ this._idle = [];
542
+ this._queue = [];
543
+ }
544
+ }
545
+
546
+ const _speculativePool = new WorkerPool(1);
547
+ _speculativePool._spawn();
548
+
182
549
  class CapWidget extends HTMLElement {
183
- #workerUrl = "";
184
550
  #resetTimer = null;
185
551
  #workersCount = navigator.hardwareConcurrency || 8;
186
552
  token = null;
@@ -221,13 +587,7 @@
221
587
  }
222
588
 
223
589
  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
- );
590
+ _getSharedWorkerUrl();
231
591
  }
232
592
 
233
593
  attributeChangedCallback(name, _, value) {
@@ -277,6 +637,15 @@
277
637
  this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
278
638
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
279
639
  this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
640
+
641
+ if (speculative.state === "idle" || speculative.state === "waiting") {
642
+ if (_speculativeTimer) {
643
+ clearTimeout(_speculativeTimer);
644
+ _speculativeTimer = null;
645
+ }
646
+ speculative.state = "waiting";
647
+ _speculativeTimer = setTimeout(() => _beginSpeculativeSolve(), SPECULATIVE_DELAY_MS);
648
+ }
280
649
  }
281
650
 
282
651
  async solve() {
@@ -287,106 +656,177 @@
287
656
  try {
288
657
  this.#solving = true;
289
658
  this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
290
-
291
659
  this.#div.setAttribute(
292
660
  "aria-label",
293
661
  this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"),
294
662
  );
295
-
296
663
  this.dispatchEvent("progress", { progress: 0 });
297
664
 
665
+ if (hasHaptics) navigator.vibrate(5);
666
+
298
667
  try {
299
668
  let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
300
-
301
669
  if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
302
670
  apiEndpoint = "/";
303
- } else if (!apiEndpoint)
671
+ } else if (!apiEndpoint) {
304
672
  throw new Error(
305
673
  "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
306
674
  );
307
-
308
- if (!apiEndpoint.endsWith("/")) {
309
- apiEndpoint += "/";
310
675
  }
676
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
311
677
 
312
- const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
313
- method: "POST",
314
- });
315
-
678
+ let solutions;
316
679
  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
680
 
323
- if (challengeResp.error) {
324
- throw new Error(challengeResp.error);
681
+ if (
682
+ speculative.state === "done" &&
683
+ speculative.token &&
684
+ speculative.tokenExpires &&
685
+ Date.now() < speculative.tokenExpires
686
+ ) {
687
+ this.dispatchEvent("progress", { progress: 100 });
688
+
689
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
690
+ if (this.querySelector(`input[name='${fieldName}']`)) {
691
+ this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
692
+ }
693
+ this.dispatchEvent("solve", { token: speculative.token });
694
+ this.token = speculative.token;
695
+
696
+ const expiresIn = speculative.tokenExpires - Date.now();
697
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
698
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
699
+
700
+ this.#div.setAttribute(
701
+ "aria-label",
702
+ this.getI18nText(
703
+ "verified-aria-label",
704
+ "We have verified you're a human, you may now continue",
705
+ ),
706
+ );
707
+ if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
708
+
709
+ _resetSpeculativeState();
710
+ this.#solving = false;
711
+ return;
325
712
  }
326
713
 
327
- const { challenge, token } = challengeResp;
714
+ if (speculative.state === "done") {
715
+ solutions = speculative.results;
716
+ challengeResp = speculative.challengeResp;
717
+ this.dispatchEvent("progress", { progress: 100 });
718
+ } else if (
719
+ speculative.state === "solving" ||
720
+ speculative.state === "redeeming" ||
721
+ speculative.state === "fetching" ||
722
+ speculative.state === "waiting"
723
+ ) {
724
+ if (speculative.state === "waiting") {
725
+ if (_speculativeTimer) {
726
+ clearTimeout(_speculativeTimer);
727
+ _speculativeTimer = null;
728
+ }
729
+ speculative.state = "waiting";
730
+ _beginSpeculativeSolve();
731
+ }
328
732
 
329
- let challenges = challenge;
733
+ speculative.pendingPromotion = this.#workersCount;
734
+ if (speculative.promoteFn) {
735
+ speculative.promoteFn(this.#workersCount);
736
+ }
330
737
 
331
- if (!Array.isArray(challenges)) {
332
- let i = 0;
738
+ const progressInterval = setInterval(() => {
739
+ if (speculative.state !== "solving" && speculative.state !== "redeeming") {
740
+ clearInterval(progressInterval);
741
+ return;
742
+ }
743
+ const total = speculative.challenges ? speculative.challenges.length : 1;
744
+ const done = speculative.completedCount;
745
+ const visual =
746
+ speculative.state === "redeeming"
747
+ ? 99
748
+ : Math.min(98, Math.round((done / total) * 100));
749
+ this.dispatchEvent("progress", { progress: visual });
750
+ }, 150);
751
+
752
+ await new Promise((resolve) => speculative.onSettled(resolve));
753
+ clearInterval(progressInterval);
754
+
755
+ if (speculative.state !== "done") {
756
+ throw new Error("Speculative solve failed – please try again");
757
+ }
333
758
 
334
- challenges = Array.from({ length: challenge.c }, () => {
335
- i = i + 1;
759
+ if (
760
+ speculative.token &&
761
+ speculative.tokenExpires &&
762
+ Date.now() < speculative.tokenExpires
763
+ ) {
764
+ this.dispatchEvent("progress", { progress: 100 });
765
+
766
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
767
+ if (this.querySelector(`input[name='${fieldName}']`)) {
768
+ this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
769
+ }
770
+ this.dispatchEvent("solve", { token: speculative.token });
771
+ this.token = speculative.token;
772
+
773
+ const expiresIn = speculative.tokenExpires - Date.now();
774
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
775
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
776
+
777
+ this.#div.setAttribute(
778
+ "aria-label",
779
+ this.getI18nText(
780
+ "verified-aria-label",
781
+ "We have verified you're a human, you may now continue",
782
+ ),
783
+ );
784
+ if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
785
+
786
+ _resetSpeculativeState();
787
+ this.#solving = false;
788
+ return;
789
+ }
336
790
 
337
- return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
791
+ solutions = speculative.results;
792
+ challengeResp = speculative.challengeResp;
793
+ this.dispatchEvent("progress", { progress: 100 });
794
+ } else {
795
+ const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
796
+ method: "POST",
338
797
  });
798
+ try {
799
+ challengeResp = await challengeRaw.json();
800
+ } catch {
801
+ throw new Error("Failed to parse challenge response from server");
802
+ }
803
+ if (challengeResp.error) throw new Error(challengeResp.error);
804
+
805
+ const { challenge, token } = challengeResp;
806
+ let challenges = challenge;
807
+ if (!Array.isArray(challenges)) {
808
+ let i = 0;
809
+ challenges = Array.from({ length: challenge.c }, () => {
810
+ i++;
811
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
812
+ });
813
+ }
814
+
815
+ solutions = await this.solveChallenges(challenges);
339
816
  }
340
817
 
341
818
  const instrPromise = challengeResp.instrumentation
342
819
  ? runInstrumentationChallenge(challengeResp.instrumentation)
343
820
  : Promise.resolve(null);
344
821
 
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]);
822
+ const instrOut = await instrPromise;
379
823
 
380
824
  if (instrOut?.__timeout || instrOut?.__blocked) {
381
- this.updateUIBlocked(
382
- this.getI18nText("error-label", "Error"),
383
- instrOut && instrOut.__blocked,
384
- );
825
+ this.updateUIBlocked(this.getI18nText("error-label", "Error"), instrOut?.__blocked);
385
826
  this.#div.setAttribute(
386
827
  "aria-label",
387
828
  this.getI18nText("error-aria-label", "An error occurred, please try again"),
388
829
  );
389
-
390
830
  this.removeEventListener("error", this.boundHandleError);
391
831
  const errEvent = new CustomEvent("error", {
392
832
  bubbles: true,
@@ -395,13 +835,14 @@
395
835
  });
396
836
  super.dispatchEvent(errEvent);
397
837
  this.addEventListener("error", this.boundHandleError);
398
-
399
838
  this.executeAttributeCode("onerror", errEvent);
400
839
  console.error("[cap]", "Instrumentation failed");
401
840
  this.#solving = false;
402
841
  return;
403
842
  }
404
843
 
844
+ const { token } = challengeResp;
845
+
405
846
  const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
406
847
  method: "POST",
407
848
  body: JSON.stringify({
@@ -421,6 +862,7 @@
421
862
 
422
863
  this.dispatchEvent("progress", { progress: 100 });
423
864
  if (!resp.success) throw new Error(resp.error || "Invalid solution");
865
+
424
866
  const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
425
867
  if (this.querySelector(`input[name='${fieldName}']`)) {
426
868
  this.querySelector(`input[name='${fieldName}']`).value = resp.token;
@@ -429,6 +871,8 @@
429
871
  this.dispatchEvent("solve", { token: resp.token });
430
872
  this.token = resp.token;
431
873
 
874
+ _resetSpeculativeState();
875
+
432
876
  if (this.#resetTimer) clearTimeout(this.#resetTimer);
433
877
  const expiresIn = new Date(resp.expires).getTime() - Date.now();
434
878
  if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
@@ -444,6 +888,7 @@
444
888
  "We have verified you're a human, you may now continue",
445
889
  ),
446
890
  );
891
+ if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
447
892
 
448
893
  return { success: true, token: this.token };
449
894
  } catch (err) {
@@ -463,6 +908,8 @@
463
908
  const total = challenges.length;
464
909
  let completed = 0;
465
910
 
911
+ const speculativeHead = 0;
912
+
466
913
  let wasmModule = null;
467
914
  const wasmSupported =
468
915
  typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
@@ -492,67 +939,31 @@
492
939
  }
493
940
  }
494
941
 
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
- });
942
+ const pool = new WorkerPool(this.#workersCount);
943
+ pool.setWasm(wasmModule);
944
+ pool._ensureSize(this.#workersCount);
536
945
 
537
946
  const results = [];
538
947
  try {
539
948
  for (let i = 0; i < challenges.length; i += this.#workersCount) {
540
949
  const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length));
541
950
  const chunkResults = await Promise.all(
542
- chunk.map((c, idx) => solveSingleChallenge(c, idx)),
951
+ chunk.map(([salt, target]) =>
952
+ pool.run(salt, target).then((nonce) => {
953
+ completed++;
954
+ const visual = Math.min(
955
+ 99,
956
+ Math.round(((speculativeHead + completed) / total) * 100),
957
+ );
958
+ this.dispatchEvent("progress", { progress: visual });
959
+ return nonce;
960
+ }),
961
+ ),
543
962
  );
544
963
  results.push(...chunkResults);
545
964
  }
546
965
  } 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
- });
966
+ pool.terminate();
556
967
  }
557
968
 
558
969
  return results;
@@ -645,7 +1056,9 @@
645
1056
  if (current) {
646
1057
  current.classList.remove("active");
647
1058
  current.classList.add("exit");
648
- current.addEventListener("transitionend", () => current.remove(), { once: true });
1059
+ current.addEventListener("transitionend", () => current.remove(), {
1060
+ once: true,
1061
+ });
649
1062
  }
650
1063
  }
651
1064
 
@@ -689,11 +1102,14 @@
689
1102
  if (current) {
690
1103
  current.classList.remove("active");
691
1104
  current.classList.add("exit");
692
- current.addEventListener("transitionend", () => current.remove(), { once: true });
1105
+ current.addEventListener("transitionend", () => current.remove(), {
1106
+ once: true,
1107
+ });
693
1108
  }
694
1109
 
695
1110
  const link = next.querySelector(".cap-troubleshoot-link");
696
1111
  if (link) {
1112
+ console.log("linkblud");
697
1113
  link.addEventListener("click", (e) => {
698
1114
  e.stopPropagation();
699
1115
  });
@@ -715,7 +1131,7 @@
715
1131
  if (wrapper) {
716
1132
  const activeLabel = wrapper.querySelector(".label.active");
717
1133
  if (activeLabel) {
718
- activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")} ${event.detail.progress}%`;
1134
+ activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")}`;
719
1135
  }
720
1136
  }
721
1137
 
@@ -730,6 +1146,8 @@
730
1146
  handleError(event) {
731
1147
  this.updateUI("error", this.getI18nText("error-label", "Error. Try again."));
732
1148
  this.executeAttributeCode("onerror", event);
1149
+
1150
+ if (hasHaptics) navigator.vibrate([10, 40, 10]);
733
1151
  }
734
1152
 
735
1153
  handleReset(event) {
@@ -742,8 +1160,10 @@
742
1160
  if (!code) {
743
1161
  return;
744
1162
  }
745
-
746
- console.error("[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.");
1163
+
1164
+ console.error(
1165
+ "[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.",
1166
+ );
747
1167
 
748
1168
  new Function("event", code).call(this, event);
749
1169
  }
@@ -803,11 +1223,6 @@
803
1223
  clearTimeout(this.#resetTimer);
804
1224
  this.#resetTimer = null;
805
1225
  }
806
-
807
- if (this.#workerUrl) {
808
- URL.revokeObjectURL(this.#workerUrl);
809
- this.#workerUrl = "";
810
- }
811
1226
  }
812
1227
  }
813
1228