ported common async to jsdoc

+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
··· 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
··· 49 49 "eslint-plugin-react": "^7.37.5", 50 50 "eslint-plugin-react-hooks": "^5.2.0", 51 51 "globals": "^16.2.0", 52 + "tidy-jsdoc": "^1.4.1", 52 53 "typescript": "^5.8.3", 53 54 "typescript-eslint-language-service": "^5.0.5", 54 55 "vite": "^6.3.5",
+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
··· 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
··· 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
··· 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
··· 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 + }