+4
-10
eslint.config.js
+4
-10
eslint.config.js
···
40
40
},
41
41
rules: {
42
42
'max-len': ['warn', { code: 100 }],
43
-
'no-multi-spaces': [
44
-
'warn',
45
-
{
46
-
exceptions: {
47
-
VariableDeclarator: true,
48
-
Property: true,
49
-
ImportAttribute: true,
50
-
},
51
-
},
52
-
],
43
+
'no-multi-spaces': ['off'],
53
44
'no-restricted-globals': ['error', ...restrictedGlobals],
54
45
'@stylistic/dot-location': ['error', 'property'],
46
+
'@stylistic/padded-blocks': ['warn', { classes: 'always', blocks: 'never' }],
47
+
'jsdoc/check-indentation': ['warn'],
48
+
'jsdoc/tag-lines': ['warn', 'always', { count: 0, startLines: 1 }],
55
49
},
56
50
},
57
51
+267
package-lock.json
+267
package-lock.json
···
29
29
"eslint-plugin-react": "^7.37.5",
30
30
"eslint-plugin-react-hooks": "^5.2.0",
31
31
"globals": "^16.2.0",
32
+
"tidy-jsdoc": "^1.4.1",
32
33
"typescript": "^5.8.3",
33
34
"typescript-eslint-language-service": "^5.0.5",
34
35
"vite": "^6.3.5",
···
1732
1733
"dev": true,
1733
1734
"license": "MIT"
1734
1735
},
1736
+
"node_modules/@types/linkify-it": {
1737
+
"version": "5.0.0",
1738
+
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
1739
+
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
1740
+
"dev": true,
1741
+
"license": "MIT"
1742
+
},
1743
+
"node_modules/@types/markdown-it": {
1744
+
"version": "12.2.3",
1745
+
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
1746
+
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
1747
+
"dev": true,
1748
+
"license": "MIT",
1749
+
"dependencies": {
1750
+
"@types/linkify-it": "*",
1751
+
"@types/mdurl": "*"
1752
+
}
1753
+
},
1735
1754
"node_modules/@types/mdast": {
1736
1755
"version": "4.0.4",
1737
1756
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
···
1741
1760
"dependencies": {
1742
1761
"@types/unist": "*"
1743
1762
}
1763
+
},
1764
+
"node_modules/@types/mdurl": {
1765
+
"version": "2.0.0",
1766
+
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
1767
+
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
1768
+
"dev": true,
1769
+
"license": "MIT"
1744
1770
},
1745
1771
"node_modules/@types/ms": {
1746
1772
"version": "2.1.0",
···
2241
2267
"url": "https://github.com/sponsors/sindresorhus"
2242
2268
}
2243
2269
},
2270
+
"node_modules/bluebird": {
2271
+
"version": "3.7.2",
2272
+
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
2273
+
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
2274
+
"dev": true,
2275
+
"license": "MIT"
2276
+
},
2244
2277
"node_modules/boolbase": {
2245
2278
"version": "1.0.0",
2246
2279
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
···
2385
2418
],
2386
2419
"license": "CC-BY-4.0"
2387
2420
},
2421
+
"node_modules/catharsis": {
2422
+
"version": "0.9.0",
2423
+
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
2424
+
"integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
2425
+
"dev": true,
2426
+
"license": "MIT",
2427
+
"dependencies": {
2428
+
"lodash": "^4.17.15"
2429
+
},
2430
+
"engines": {
2431
+
"node": ">= 10"
2432
+
}
2433
+
},
2388
2434
"node_modules/ccount": {
2389
2435
"version": "2.0.1",
2390
2436
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
···
3822
3868
"node": ">=0.8.19"
3823
3869
}
3824
3870
},
3871
+
"node_modules/inherits": {
3872
+
"version": "2.0.3",
3873
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
3874
+
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
3875
+
"dev": true,
3876
+
"license": "ISC"
3877
+
},
3825
3878
"node_modules/internal-slot": {
3826
3879
"version": "1.1.0",
3827
3880
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
···
4302
4355
"js-yaml": "bin/js-yaml.js"
4303
4356
}
4304
4357
},
4358
+
"node_modules/js2xmlparser": {
4359
+
"version": "4.0.2",
4360
+
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
4361
+
"integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
4362
+
"dev": true,
4363
+
"license": "Apache-2.0",
4364
+
"dependencies": {
4365
+
"xmlcreate": "^2.0.4"
4366
+
}
4367
+
},
4368
+
"node_modules/jsdoc": {
4369
+
"version": "3.6.11",
4370
+
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz",
4371
+
"integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==",
4372
+
"dev": true,
4373
+
"license": "Apache-2.0",
4374
+
"dependencies": {
4375
+
"@babel/parser": "^7.9.4",
4376
+
"@types/markdown-it": "^12.2.3",
4377
+
"bluebird": "^3.7.2",
4378
+
"catharsis": "^0.9.0",
4379
+
"escape-string-regexp": "^2.0.0",
4380
+
"js2xmlparser": "^4.0.2",
4381
+
"klaw": "^3.0.0",
4382
+
"markdown-it": "^12.3.2",
4383
+
"markdown-it-anchor": "^8.4.1",
4384
+
"marked": "^4.0.10",
4385
+
"mkdirp": "^1.0.4",
4386
+
"requizzle": "^0.2.3",
4387
+
"strip-json-comments": "^3.1.0",
4388
+
"taffydb": "2.6.2",
4389
+
"underscore": "~1.13.2"
4390
+
},
4391
+
"bin": {
4392
+
"jsdoc": "jsdoc.js"
4393
+
},
4394
+
"engines": {
4395
+
"node": ">=12.0.0"
4396
+
}
4397
+
},
4305
4398
"node_modules/jsdoc-type-pratt-parser": {
4306
4399
"version": "4.1.0",
4307
4400
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
···
4312
4405
"node": ">=12.0.0"
4313
4406
}
4314
4407
},
4408
+
"node_modules/jsdoc/node_modules/escape-string-regexp": {
4409
+
"version": "2.0.0",
4410
+
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
4411
+
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
4412
+
"dev": true,
4413
+
"license": "MIT",
4414
+
"engines": {
4415
+
"node": ">=8"
4416
+
}
4417
+
},
4418
+
"node_modules/jsdoc/node_modules/taffydb": {
4419
+
"version": "2.6.2",
4420
+
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
4421
+
"integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==",
4422
+
"dev": true
4423
+
},
4315
4424
"node_modules/jsesc": {
4316
4425
"version": "3.1.0",
4317
4426
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
···
4392
4501
"json-buffer": "3.0.1"
4393
4502
}
4394
4503
},
4504
+
"node_modules/klaw": {
4505
+
"version": "3.0.0",
4506
+
"resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
4507
+
"integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
4508
+
"dev": true,
4509
+
"license": "MIT",
4510
+
"dependencies": {
4511
+
"graceful-fs": "^4.1.9"
4512
+
}
4513
+
},
4395
4514
"node_modules/kolorist": {
4396
4515
"version": "1.8.0",
4397
4516
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
···
4413
4532
"node": ">= 0.8.0"
4414
4533
}
4415
4534
},
4535
+
"node_modules/linkify-it": {
4536
+
"version": "3.0.3",
4537
+
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
4538
+
"integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
4539
+
"dev": true,
4540
+
"license": "MIT",
4541
+
"dependencies": {
4542
+
"uc.micro": "^1.0.1"
4543
+
}
4544
+
},
4416
4545
"node_modules/locate-path": {
4417
4546
"version": "6.0.0",
4418
4547
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
···
4429
4558
"url": "https://github.com/sponsors/sindresorhus"
4430
4559
}
4431
4560
},
4561
+
"node_modules/lodash": {
4562
+
"version": "4.17.21",
4563
+
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
4564
+
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
4565
+
"dev": true,
4566
+
"license": "MIT"
4567
+
},
4432
4568
"node_modules/lodash.merge": {
4433
4569
"version": "4.6.2",
4434
4570
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
···
4480
4616
"@jridgewell/sourcemap-codec": "^1.5.0"
4481
4617
}
4482
4618
},
4619
+
"node_modules/markdown-it": {
4620
+
"version": "12.3.2",
4621
+
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
4622
+
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
4623
+
"dev": true,
4624
+
"license": "MIT",
4625
+
"dependencies": {
4626
+
"argparse": "^2.0.1",
4627
+
"entities": "~2.1.0",
4628
+
"linkify-it": "^3.0.1",
4629
+
"mdurl": "^1.0.1",
4630
+
"uc.micro": "^1.0.5"
4631
+
},
4632
+
"bin": {
4633
+
"markdown-it": "bin/markdown-it.js"
4634
+
}
4635
+
},
4636
+
"node_modules/markdown-it-anchor": {
4637
+
"version": "8.6.7",
4638
+
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
4639
+
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
4640
+
"dev": true,
4641
+
"license": "Unlicense",
4642
+
"peerDependencies": {
4643
+
"@types/markdown-it": "*",
4644
+
"markdown-it": "*"
4645
+
}
4646
+
},
4647
+
"node_modules/markdown-it/node_modules/entities": {
4648
+
"version": "2.1.0",
4649
+
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
4650
+
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
4651
+
"dev": true,
4652
+
"license": "BSD-2-Clause",
4653
+
"funding": {
4654
+
"url": "https://github.com/fb55/entities?sponsor=1"
4655
+
}
4656
+
},
4483
4657
"node_modules/markdown-table": {
4484
4658
"version": "3.0.4",
4485
4659
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
···
4489
4663
"funding": {
4490
4664
"type": "github",
4491
4665
"url": "https://github.com/sponsors/wooorm"
4666
+
}
4667
+
},
4668
+
"node_modules/marked": {
4669
+
"version": "4.3.0",
4670
+
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
4671
+
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
4672
+
"dev": true,
4673
+
"license": "MIT",
4674
+
"bin": {
4675
+
"marked": "bin/marked.js"
4676
+
},
4677
+
"engines": {
4678
+
"node": ">= 12"
4492
4679
}
4493
4680
},
4494
4681
"node_modules/math-intrinsics": {
···
4752
4939
"integrity": "sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==",
4753
4940
"dev": true,
4754
4941
"license": "CC0-1.0"
4942
+
},
4943
+
"node_modules/mdurl": {
4944
+
"version": "1.0.1",
4945
+
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
4946
+
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
4947
+
"dev": true,
4948
+
"license": "MIT"
4755
4949
},
4756
4950
"node_modules/merge2": {
4757
4951
"version": "1.4.1",
···
5401
5595
"url": "https://github.com/sponsors/isaacs"
5402
5596
}
5403
5597
},
5598
+
"node_modules/mkdirp": {
5599
+
"version": "1.0.4",
5600
+
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
5601
+
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
5602
+
"dev": true,
5603
+
"license": "MIT",
5604
+
"bin": {
5605
+
"mkdirp": "bin/cmd.js"
5606
+
},
5607
+
"engines": {
5608
+
"node": ">=10"
5609
+
}
5610
+
},
5404
5611
"node_modules/ms": {
5405
5612
"version": "2.1.3",
5406
5613
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
···
5953
6160
},
5954
6161
"funding": {
5955
6162
"url": "https://github.com/sponsors/ljharb"
6163
+
}
6164
+
},
6165
+
"node_modules/requizzle": {
6166
+
"version": "0.2.4",
6167
+
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
6168
+
"integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
6169
+
"dev": true,
6170
+
"license": "MIT",
6171
+
"dependencies": {
6172
+
"lodash": "^4.17.21"
5956
6173
}
5957
6174
},
5958
6175
"node_modules/resolve": {
···
6527
6744
"url": "https://github.com/sponsors/ljharb"
6528
6745
}
6529
6746
},
6747
+
"node_modules/taffydb": {
6748
+
"version": "2.7.3",
6749
+
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.7.3.tgz",
6750
+
"integrity": "sha512-GQ3gtYFSOAxSMN/apGtDKKkbJf+8izz5YfbGqIsUc7AMiQOapARZ76dhilRY2h39cynYxBFdafQo5HUL5vgkrg==",
6751
+
"dev": true,
6752
+
"license": "BSD-2-Clause"
6753
+
},
6754
+
"node_modules/tidy-jsdoc": {
6755
+
"version": "1.4.1",
6756
+
"resolved": "https://registry.npmjs.org/tidy-jsdoc/-/tidy-jsdoc-1.4.1.tgz",
6757
+
"integrity": "sha512-FpH1oL6fEMMO0qPPAjoV8peAriwTjdys92TMsfMufrDERDGfmg2w90ieqOQ4RGDH7yuvDTqxR7a0W1Mfun8fzA==",
6758
+
"dev": true,
6759
+
"license": "Apache-2.0",
6760
+
"dependencies": {
6761
+
"jsdoc": "^3.6.3",
6762
+
"taffydb": "^2.7.3",
6763
+
"util": "^0.10.3"
6764
+
}
6765
+
},
6530
6766
"node_modules/tiny-invariant": {
6531
6767
"version": "1.3.3",
6532
6768
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
···
6722
6958
"typescript": ">= 4.0.0"
6723
6959
}
6724
6960
},
6961
+
"node_modules/uc.micro": {
6962
+
"version": "1.0.6",
6963
+
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
6964
+
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
6965
+
"dev": true,
6966
+
"license": "MIT"
6967
+
},
6725
6968
"node_modules/unbox-primitive": {
6726
6969
"version": "1.1.0",
6727
6970
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
···
6740
6983
"funding": {
6741
6984
"url": "https://github.com/sponsors/ljharb"
6742
6985
}
6986
+
},
6987
+
"node_modules/underscore": {
6988
+
"version": "1.13.7",
6989
+
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
6990
+
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
6991
+
"dev": true,
6992
+
"license": "MIT"
6743
6993
},
6744
6994
"node_modules/undici-types": {
6745
6995
"version": "7.8.0",
···
6859
7109
"license": "BSD-2-Clause",
6860
7110
"dependencies": {
6861
7111
"punycode": "^2.1.0"
7112
+
}
7113
+
},
7114
+
"node_modules/util": {
7115
+
"version": "0.10.4",
7116
+
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
7117
+
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
7118
+
"dev": true,
7119
+
"license": "MIT",
7120
+
"dependencies": {
7121
+
"inherits": "2.0.3"
6862
7122
}
6863
7123
},
6864
7124
"node_modules/vite": {
···
7276
7536
"engines": {
7277
7537
"node": ">=0.10.0"
7278
7538
}
7539
+
},
7540
+
"node_modules/xmlcreate": {
7541
+
"version": "2.0.4",
7542
+
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
7543
+
"integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
7544
+
"dev": true,
7545
+
"license": "Apache-2.0"
7279
7546
},
7280
7547
"node_modules/yallist": {
7281
7548
"version": "3.1.1",
+1
package.json
+1
package.json
+64
src/common/async/aborts.js
+64
src/common/async/aborts.js
···
1
+
/** @module common/async */
2
+
3
+
/**
4
+
* @typedef TimeoutSignal
5
+
* @property {AbortSignal} signal the signal to pass down to blocking operations.
6
+
* @property {function(): void} cleanup a function to call when the timeout is no longer necessary
7
+
*/
8
+
9
+
/**
10
+
* Create an abort signal tied to a timeout.
11
+
* Replaces `AbortSignal.timeout`, which doesn't consistently abort with a TimeoutError cross env.
12
+
*
13
+
* @param {number} ms - timeout in milliseconds
14
+
* @returns {TimeoutSignal} the timeout signal
15
+
*/
16
+
export function timeoutSignal(ms) {
17
+
const controller = new AbortController()
18
+
const timeout = setTimeout(() => {
19
+
controller.abort(new DOMException('Operation timed out', 'TimeoutError'))
20
+
}, ms)
21
+
22
+
const cleanup = () => {
23
+
clearTimeout(timeout)
24
+
controller.signal.removeEventListener('abort', cleanup)
25
+
}
26
+
27
+
controller.signal.addEventListener('abort', cleanup)
28
+
return { signal: controller.signal, cleanup }
29
+
}
30
+
31
+
/**
32
+
* Create an abort signal that aborts if any of the passed signals aborts.
33
+
* Better than managing them manually because we do proper cleanup of non-triggered aborts.
34
+
*
35
+
* @param {...AbortSignal} signals - the signals to combine
36
+
* @returns {AbortSignal} the combined signal
37
+
*/
38
+
export function combineSignals(...signals) {
39
+
const controller = new AbortController()
40
+
/** @type { Array<function(): void> } */
41
+
const cleanups = []
42
+
43
+
for (const signal of signals) {
44
+
if (signal.aborted) {
45
+
controller.abort(signal.reason)
46
+
return controller.signal
47
+
}
48
+
49
+
const handler = () => {
50
+
if (!controller.signal.aborted) {
51
+
controller.abort(signal.reason)
52
+
}
53
+
}
54
+
55
+
signal.addEventListener('abort', handler)
56
+
cleanups.push(() => signal.removeEventListener('abort', handler))
57
+
}
58
+
59
+
controller.signal.addEventListener('abort', () => {
60
+
cleanups.forEach(cb => cb())
61
+
})
62
+
63
+
return controller.signal
64
+
}
+53
src/common/async/blocking-atom.js
+53
src/common/async/blocking-atom.js
···
1
+
/** @module common/async */
2
+
3
+
import { Semaphore } from './semaphore.js'
4
+
5
+
/**
6
+
* simple blocking atom, for waiting for a value.
7
+
* cribbed mostly from {@link https://github.com/ComFreek/async-playground}
8
+
*
9
+
* @template T - the type we're holding
10
+
*/
11
+
export class BlockingAtom {
12
+
13
+
/** @type {T | undefined} */
14
+
#item
15
+
16
+
/** @type {Semaphore} */
17
+
#sema
18
+
19
+
constructor() {
20
+
this.#sema = new Semaphore()
21
+
this.#item = undefined
22
+
}
23
+
24
+
/**
25
+
* puts an item into the atom and unblocks an awaiter.
26
+
*
27
+
* @param {T} item the item to put into the atom
28
+
*/
29
+
set(item) {
30
+
this.#item = item
31
+
this.#sema.free()
32
+
}
33
+
34
+
/**
35
+
* tries to get the item from the atom, and blocks until available.
36
+
*
37
+
* @example
38
+
* if (await atom.take())
39
+
* console.log('got it!')
40
+
*
41
+
* @param {AbortSignal | undefined} signal - an abort signal to cancel the await
42
+
* @returns {Promise<T | undefined>} a promise for the item, or undefined if something aborted.
43
+
*/
44
+
async get(signal) {
45
+
if (await this.#sema.take(signal)) {
46
+
return this.#item
47
+
}
48
+
49
+
signal?.throwIfAborted()
50
+
return undefined
51
+
}
52
+
53
+
}
+67
src/common/async/blocking-queue.js
+67
src/common/async/blocking-queue.js
···
1
+
/** @module common/async */
2
+
3
+
import { Semaphore } from './semaphore.js'
4
+
5
+
/**
6
+
* simple blocking queue, for turning streams into async pulls.
7
+
* cribbed mostly from {@link https://github.com/ComFreek/async-playground}
8
+
*
9
+
* @template T
10
+
*/
11
+
export class BlockingQueue {
12
+
13
+
/** @type {Semaphore} */
14
+
#sema
15
+
16
+
/** @type {T[]} */
17
+
#items
18
+
19
+
/** @type {number | undefined} */
20
+
#maxsize
21
+
22
+
constructor(maxsize = 1000) {
23
+
this.#sema = new Semaphore()
24
+
this.#items = []
25
+
this.#maxsize = maxsize ? maxsize : undefined
26
+
}
27
+
28
+
/**
29
+
* place one or more items on the queue, to be picked up by awaiters.
30
+
*
31
+
* @param {...T} elements the items to place on the queue.
32
+
*/
33
+
enqueue(...elements) {
34
+
for (const el of elements) {
35
+
if (this.#maxsize && this.#items.length >= this.#maxsize) {
36
+
throw Error('out of room')
37
+
}
38
+
39
+
this.#items.push(el)
40
+
this.#sema.free()
41
+
}
42
+
}
43
+
44
+
/**
45
+
* block while waiting for an item off the queue.
46
+
*
47
+
* @param {AbortSignal} [signal] a signal to use for aborting the block.
48
+
* @returns {Promise<T>} the item off the queue; rejects if aborted.
49
+
*/
50
+
async dequeue(signal) {
51
+
if (await this.#sema.take(signal)) {
52
+
return this.#poll()
53
+
}
54
+
55
+
signal?.throwIfAborted()
56
+
throw Error('canceled dequeue')
57
+
}
58
+
59
+
#poll() {
60
+
const item = this.#items.length > 0 && this.#items.shift()
61
+
if (item)
62
+
return item
63
+
64
+
throw Error('no elements')
65
+
}
66
+
67
+
}
+82
src/common/async/gate.js
+82
src/common/async/gate.js
···
1
+
/** @module common/async */
2
+
3
+
/**
4
+
* A Breaker, which allows creating wrapped functions which will only be executed before
5
+
* the breaker is tripped.
6
+
*
7
+
* @example
8
+
* const breaker = makeBreaker()
9
+
*
10
+
* state.addEventHandler('finish', breaker.tripThen((e) => {
11
+
* // this will only be allowed to run once
12
+
* // the second time the event fired, the handler is a no-op
13
+
* })
14
+
*
15
+
* state.addEventHandler('error', breaker.tripThen((e) => {
16
+
* // all wrapped functions created by the same breaker share state
17
+
* // so if the above fired, this can never be called
18
+
* })
19
+
*
20
+
* state.addEventHandler('message', breaker.untilTripped((e) => {
21
+
* // this will only be allowed to run many times
22
+
* // but not *after* any of the _once_ wrappers has been called
23
+
* })
24
+
*/
25
+
export class Breaker {
26
+
27
+
/** @type {undefined | VoidCallback} */
28
+
#onTripped
29
+
30
+
/** @type {boolean} */
31
+
#tripped
32
+
33
+
/**
34
+
* @param {VoidCallback} [onTripped]
35
+
* an optional callback, called when the breaker is tripped, /before/ any wrapped functions.
36
+
*/
37
+
constructor(onTripped) {
38
+
this.#tripped = false
39
+
this.#onTripped = onTripped
40
+
}
41
+
42
+
/** @returns {boolean} true if the breaker has already tripped */
43
+
tripped() {
44
+
return this.#tripped
45
+
}
46
+
47
+
/**
48
+
* wrap the given callback in a function that will trip the breaker before it's called.
49
+
* any subsequent calls to the wrapped function will be no-ops.
50
+
*
51
+
* @param {Callback} fn the function to be wrapped in the breaker
52
+
* @returns {Callback} a wrapped function, controlled by the breaker
53
+
*/
54
+
tripThen(fn) {
55
+
return (...args) => {
56
+
if (!this.#tripped) {
57
+
this.#tripped = true
58
+
59
+
// TODO: if these throw, what to do?
60
+
this.#onTripped?.()
61
+
fn(...args)
62
+
}
63
+
}
64
+
}
65
+
66
+
/**
67
+
* wrap the given callback in a function that check the breaker before it's called.
68
+
* once the breaker has been tripped, calls to the wrapped function will be no-ops.
69
+
*
70
+
* @param {Callback} fn the function to be wrapped in the breaker
71
+
* @returns {Callback} a wrapped function, controlled by the breaker
72
+
*/
73
+
untilTripped(fn) {
74
+
return (...args) => {
75
+
if (!this.#tripped) {
76
+
// TODO: if these throw, what to do?
77
+
fn(...args)
78
+
}
79
+
}
80
+
}
81
+
82
+
}
+73
src/common/async/semaphore.js
+73
src/common/async/semaphore.js
···
1
+
/** @module common/async */
2
+
3
+
/**
4
+
* Simple counting semaphore, for blocking async ops.
5
+
* cribbed mostly from {@link https://github.com/ComFreek/async-playground}
6
+
*/
7
+
export class Semaphore {
8
+
9
+
/** @type { number } */
10
+
#counter = 0
11
+
12
+
/** @type {Array<function(boolean): void>} */
13
+
#resolvers = []
14
+
15
+
constructor(count = 0) {
16
+
this.#counter = count
17
+
}
18
+
19
+
/**
20
+
* try to take from the semaphore, reducing it's count
21
+
* if the semaphore is empty, blocks until available, or the given signal aborts.
22
+
*
23
+
* @param {AbortSignal | undefined} signal a signal to use to abort the block
24
+
* @returns {Promise<boolean>} true if the semaphore was successfully taken, false if aborted.
25
+
*/
26
+
take(signal) {
27
+
return new Promise((resolve) => {
28
+
if (signal?.aborted) return resolve(false)
29
+
30
+
// if there's resources available, use them
31
+
32
+
this.#counter--
33
+
if (this.#counter >= 0) return resolve(true)
34
+
35
+
// otherwise add to pending
36
+
// and explicitly remove the resolver from the list on abort
37
+
38
+
this.#resolvers.push(resolve)
39
+
signal?.addEventListener('abort', () => {
40
+
const index = this.#resolvers.indexOf(resolve)
41
+
if (index >= 0) {
42
+
this.#resolvers.splice(index, 1)
43
+
this.#counter++
44
+
}
45
+
46
+
resolve(false)
47
+
})
48
+
})
49
+
}
50
+
51
+
/**
52
+
* try to take from the semaphore, reducing it's count, *without blocking*.
53
+
*
54
+
* @returns {boolean} true if the semaphore was taken, false otherwise.
55
+
*/
56
+
poll() {
57
+
if (this.#counter <= 0) return false
58
+
59
+
this.#counter--
60
+
return true
61
+
}
62
+
63
+
/** announce that the semaphore is free to be taken by another awaiter. */
64
+
free() {
65
+
this.#counter++
66
+
67
+
if (this.#resolvers.length > 0) {
68
+
const resolver = this.#resolvers.shift()
69
+
resolver && queueMicrotask(() => resolver(true))
70
+
}
71
+
}
72
+
73
+
}