Doc commit tag conventions
This commit is contained in:
+131
-1
@@ -1 +1,131 @@
|
|||||||
TODO
|
# Guidelines
|
||||||
|
|
||||||
|
## MR/Commit Messages
|
||||||
|
|
||||||
|
NOTE: Not necessary to follow this until V1.0 release.
|
||||||
|
|
||||||
|
Use a [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) format when creating MR's. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(Primitives): Add Transaction Primitive
|
||||||
|
```
|
||||||
|
|
||||||
|
... or to indicate a breaking change (using `!`):
|
||||||
|
|
||||||
|
```
|
||||||
|
chore!: Drop support for Node 16
|
||||||
|
```
|
||||||
|
|
||||||
|
This will ensure that we can automatically generate correct [Semantic Versions](https://semver.org/) and changelogs between releases (using Lerna).
|
||||||
|
|
||||||
|
A table of common prefixes (conventions) is below:
|
||||||
|
|
||||||
|
| Change | Description |
|
||||||
|
|----------|-------------------------------------------------------------------------------------------------------------|
|
||||||
|
| feat | A new feature |
|
||||||
|
| fix | A bug fix |
|
||||||
|
| docs | Documentation only changes |
|
||||||
|
| style | Changes that do not affect the meaning of the code (formatting) |
|
||||||
|
| refactor | A code change that neither fixes a bug nor adds a feature |
|
||||||
|
| perf | A code change that improves performance |
|
||||||
|
| test | Adding missing tests or correct existing tests |
|
||||||
|
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) |
|
||||||
|
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) |
|
||||||
|
| chore | Other changes that don't modify src or test files |
|
||||||
|
| revert | Reverts a previous commit |
|
||||||
|
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
We use Dependency Injection (DI) for several key reasons:
|
||||||
|
|
||||||
|
- Improved testability: Dependencies can be easily mocked during unit tests
|
||||||
|
- Reduced coupling: Classes depend on abstractions rather than concrete implementations
|
||||||
|
- Greater flexibility: Implementations can be swapped without modifying dependent classes
|
||||||
|
- Clearer dependencies: Constructor signatures explicitly document required dependencies
|
||||||
|
- Easier refactoring: Component relationships are clearly defined
|
||||||
|
- Simpler dependency management: Easier control over resource initialization and lifecycle
|
||||||
|
|
||||||
|
Try not to over-abstract, but if there is already a Base Class (or interface) available, always try to use that (via dependency injection) in favor of a concrete implementation.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class SomeClass {
|
||||||
|
protected blockchain: BaseBlockchain;
|
||||||
|
protected store: BaseStore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
//
|
||||||
|
dependencies: {
|
||||||
|
blockchain: BaseBlockchain,
|
||||||
|
store: BaseStore,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
this.blockchain = dependencies.blockchain;
|
||||||
|
this.store = dependencies.store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Factory Patterns
|
||||||
|
|
||||||
|
When creating classes, create static `from(...)` (or `fromSpecificThing(...)`) methods.
|
||||||
|
|
||||||
|
*Semantically, think of "from" as meaning "instantiate from": This is more succinct than a `create` factory method which, if we have multiple ways of instantiation, can become verbose. E.g. `PrivateKey.fromWIF()` as opposed to `PrivateKey.createFromWIF()`. It also allows us to differentiate between actually "creating" something (e.g. a `Filesystem.createFile(...)`) versus "instantiating" a representation of a "thing".*
|
||||||
|
|
||||||
|
In Javascript, a `constructor` cannot be asynchronous and we want to ensure that we can `await` class instantiation to mitigate against stateful errors.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
class SomeClass {
|
||||||
|
// Static "from" factory method.
|
||||||
|
// NOTE: The below args are shorthand for using the same arguments as the constructor.
|
||||||
|
static async from(...args: ConstructorParameters<typeof SomeClass>) {
|
||||||
|
// Create the instance.
|
||||||
|
const someClass = new SomeClass(...args);
|
||||||
|
|
||||||
|
// Execute the asynchronous initialization function.
|
||||||
|
await someClass.start();
|
||||||
|
|
||||||
|
// Return the instance.
|
||||||
|
return someClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Although this isn't always necessary (i.e. not all instantiations need to be `async`), it keeps instantiation of classes consistent.
|
||||||
|
|
||||||
|
### Validations
|
||||||
|
|
||||||
|
Check types with TypeScript - Runtime type-checking is a code smell. If the function accepts a string, assume it's been given a string. TypeScript definitions should expose improperly called functions to the developer at compile time, don't re-implement it at runtime.
|
||||||
|
|
||||||
|
There are a few exceptions to this:
|
||||||
|
|
||||||
|
- If TypeScript's lack of dependant types makes validating certain properties of a type (e.g. the length of a Uint8Array), then runtime type-checking should be used to detect and return errors.
|
||||||
|
- If we are working with a network (remote/untrusted) payload, Type Validation should be done with [Zod](https://www.npmjs.com/package/zod).
|
||||||
|
|
||||||
|
An example of working with Zod:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Define a schema so that we can validate the payload.
|
||||||
|
export const SomeRemotePayload = z.object({
|
||||||
|
publicKey: z.string(),
|
||||||
|
action: z.enum(['set', 'delete', 'clear']),
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
timestamp: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run Zod's validation against this payload.
|
||||||
|
// NOTE: This will throw if it fails.
|
||||||
|
const parsedPayload = SomeRemotePayload.parse(rawPayload);
|
||||||
|
|
||||||
|
// (Optional) If this type is needed elsewhere, we can infer it from Zod and export it.
|
||||||
|
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+24
-10
@@ -8,19 +8,37 @@
|
|||||||
"name": "@xo/node-demo",
|
"name": "@xo/node-demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xo/network-wallet": "file:../../packages/network-wallet",
|
"@xo/stack": "file:../../packages/stack"
|
||||||
"@xo/primitives": "file:../../packages/primitives"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../../packages/network-wallet": {},
|
"../../packages/network-wallet": {
|
||||||
|
"extraneous": true
|
||||||
|
},
|
||||||
"../../packages/primitives": {
|
"../../packages/primitives": {
|
||||||
"name": "@xo/primitives",
|
"name": "@xocash/primitives",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"extraneous": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"../../packages/stack": {
|
||||||
|
"name": "@xocash/stack",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@electrum-cash/network": "^4.1.1",
|
||||||
|
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
||||||
|
"@xocash/primitives": "*",
|
||||||
|
"better-sqlite3": "^11.8.1",
|
||||||
|
"idb": "^8.0.0",
|
||||||
|
"msgpackr": "^1.11.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
@@ -395,12 +413,8 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xo/network-wallet": {
|
"node_modules/@xo/stack": {
|
||||||
"resolved": "../../packages/network-wallet",
|
"resolved": "../../packages/stack",
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/@xo/primitives": {
|
|
||||||
"resolved": "../../packages/primitives",
|
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
"start": "node index.js"
|
"start": "node index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xo/primitives": "file:../../packages/primitives",
|
"@xo/stack": "file:../../packages/stack"
|
||||||
"@xo/network-wallet": "file:../../packages/network-wallet"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.0.0"
|
"vite": "^4.0.0"
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
Encrypter,
|
||||||
|
PrivateKey,
|
||||||
|
StoreRemoteHTTP,
|
||||||
|
} from '../../packages/stack/src/index.js';
|
||||||
|
|
||||||
|
const privateKey = PrivateKey.generateRandom();
|
||||||
|
const encrypter = Encrypter.generateRandom();
|
||||||
|
const storeHTTP = new StoreRemoteHTTP(
|
||||||
|
'http://localhost:3000/store',
|
||||||
|
privateKey,
|
||||||
|
encrypter,
|
||||||
|
);
|
||||||
|
|
||||||
|
const keys = await storeHTTP.keys();
|
||||||
|
|
||||||
|
console.log(keys);
|
||||||
Generated
+228
-22
@@ -3087,6 +3087,16 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "7.6.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz",
|
||||||
|
"integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3": {
|
"node_modules/@types/d3": {
|
||||||
"version": "7.4.3",
|
"version": "7.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||||
@@ -4624,7 +4634,6 @@
|
|||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4648,6 +4657,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "11.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz",
|
||||||
|
"integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bin-links": {
|
"node_modules/bin-links": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz",
|
||||||
@@ -4664,6 +4684,15 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/birpc": {
|
"node_modules/birpc": {
|
||||||
"version": "0.2.19",
|
"version": "0.2.19",
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz",
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz",
|
||||||
@@ -4678,7 +4707,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
@@ -4713,7 +4741,6 @@
|
|||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6133,6 +6160,21 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dedent": {
|
"node_modules/dedent": {
|
||||||
"version": "1.5.3",
|
"version": "1.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
|
||||||
@@ -6162,7 +6204,6 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
@@ -6279,7 +6320,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -6649,7 +6689,6 @@
|
|||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
@@ -7126,6 +7165,15 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"license": "(MIT OR WTFPL)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expect-type": {
|
"node_modules/expect-type": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
|
||||||
@@ -7278,6 +7326,12 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
@@ -7519,7 +7573,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
@@ -7833,6 +7886,12 @@
|
|||||||
"ini": "^1.3.2"
|
"ini": "^1.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.4.5",
|
"version": "10.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
@@ -8145,7 +8204,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -8258,14 +8316,12 @@
|
|||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ini": {
|
"node_modules/ini": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/init-package-json": {
|
"node_modules/init-package-json": {
|
||||||
@@ -9969,6 +10025,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/min-indent": {
|
"node_modules/min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
@@ -9999,7 +10067,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -10193,6 +10260,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mlly": {
|
"node_modules/mlly": {
|
||||||
"version": "1.7.4",
|
"version": "1.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||||
@@ -10434,6 +10507,12 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
@@ -10458,6 +10537,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.74.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||||
|
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
@@ -10832,7 +10923,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@@ -11542,6 +11632,32 @@
|
|||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/precinct": {
|
"node_modules/precinct": {
|
||||||
"version": "12.1.2",
|
"version": "12.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/precinct/-/precinct-12.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/precinct/-/precinct-12.1.2.tgz",
|
||||||
@@ -11771,6 +11887,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -11833,7 +11959,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
"dev": true,
|
|
||||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-extend": "^0.6.0",
|
"deep-extend": "^0.6.0",
|
||||||
@@ -11849,7 +11974,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -12108,7 +12232,6 @@
|
|||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -12480,7 +12603,6 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -12542,7 +12664,6 @@
|
|||||||
"version": "7.6.3",
|
"version": "7.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -12641,6 +12762,51 @@
|
|||||||
"node": "^16.14.0 || >=18.0.0"
|
"node": "^16.14.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
@@ -12853,7 +13019,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
@@ -13146,11 +13311,28 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-fs/node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
@@ -13486,6 +13668,18 @@
|
|||||||
"node": "^16.14.0 || >=18.0.0"
|
"node": "^16.14.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@@ -13778,7 +13972,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
@@ -14277,7 +14470,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
@@ -14499,6 +14691,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.24.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||||
|
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
@@ -14544,8 +14745,13 @@
|
|||||||
"@electrum-cash/network": "^4.1.1",
|
"@electrum-cash/network": "^4.1.1",
|
||||||
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
||||||
"@xocash/primitives": "*",
|
"@xocash/primitives": "*",
|
||||||
|
"better-sqlite3": "^11.8.1",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"msgpackr": "^1.11.2"
|
"msgpackr": "^1.11.2",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ export class PrivateKey {
|
|||||||
/**
|
/**
|
||||||
* Sign a message using a recoverable compact signature format.
|
* Sign a message using a recoverable compact signature format.
|
||||||
*
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* This method will SHA256 hash the message for you.
|
||||||
|
*
|
||||||
* @param message The message to sign, either as a Uint8Array or string.
|
* @param message The message to sign, either as a Uint8Array or string.
|
||||||
* If a string is provided, it will be converted to UTF-8 bytes.
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
||||||
*
|
*
|
||||||
@@ -275,6 +279,10 @@ export class PrivateKey {
|
|||||||
/**
|
/**
|
||||||
* Sign a message using a compact signature format.
|
* Sign a message using a compact signature format.
|
||||||
*
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* This method will SHA256 hash the message for you.
|
||||||
|
*
|
||||||
* @param message The message to sign, either as a Uint8Array or string.
|
* @param message The message to sign, either as a Uint8Array or string.
|
||||||
* If a string is provided, it will be converted to UTF-8 bytes.
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
||||||
*
|
*
|
||||||
@@ -295,6 +303,10 @@ export class PrivateKey {
|
|||||||
/**
|
/**
|
||||||
* Sign a message using the Schnorr signature scheme.
|
* Sign a message using the Schnorr signature scheme.
|
||||||
*
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* This method will SHA256 hash the message for you.
|
||||||
|
*
|
||||||
* @param message The message to sign, either as a Uint8Array or string.
|
* @param message The message to sign, either as a Uint8Array or string.
|
||||||
* If a string is provided, it will be converted to UTF-8 bytes.
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
||||||
*
|
*
|
||||||
@@ -315,6 +327,10 @@ export class PrivateKey {
|
|||||||
/**
|
/**
|
||||||
* Sign a message using DER (Distinguished Encoding Rules) format.
|
* Sign a message using DER (Distinguished Encoding Rules) format.
|
||||||
*
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* This method will SHA256 hash the message for you.
|
||||||
|
*
|
||||||
* @param message The message to sign, either as a Uint8Array or string.
|
* @param message The message to sign, either as a Uint8Array or string.
|
||||||
* If a string is provided, it will be converted to UTF-8 bytes.
|
* If a string is provided, it will be converted to UTF-8 bytes.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -34,7 +34,12 @@
|
|||||||
"@electrum-cash/network": "^4.1.1",
|
"@electrum-cash/network": "^4.1.1",
|
||||||
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
"@electrum-cash/protocol": "2.1.0-development.8075178089",
|
||||||
"@xocash/primitives": "*",
|
"@xocash/primitives": "*",
|
||||||
|
"better-sqlite3": "^11.8.1",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"msgpackr": "^1.11.2"
|
"msgpackr": "^1.11.2",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
export * from './index.js';
|
export * from './index.js';
|
||||||
|
|
||||||
// These exports will only work in a NodeJS (backend) environment.
|
// These exports will only work in a NodeJS (backend) environment.
|
||||||
// NOTE: We import and export each module with a common alias so that any leveraging libraries can use the common name and maintain cross-platform compatibility.
|
// TODO: FIx me!
|
||||||
// export * from './storage/storage-local.node.js';
|
// export * from './storage/store-sqlite.js';
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// A P2P Adapter does not HAVE to be LibP2P.
|
||||||
|
// We could create an adapter to use a centralized server.
|
||||||
|
export abstract class P2P {
|
||||||
|
// from(bootstrapNodes)
|
||||||
|
|
||||||
|
abstract createChannel(): Promise<void>;
|
||||||
|
|
||||||
|
abstract findPeer(peerId: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class P2PChannel {}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Encrypter, EventEmitter } from '../utils';
|
import { Encrypter, EventEmitter, InverseBloomFilter } from '../utils';
|
||||||
|
|
||||||
export type StorageOpts = {
|
export type StorageOpts = {
|
||||||
syncInMemory?: boolean;
|
syncInMemory?: boolean;
|
||||||
@@ -268,4 +268,15 @@ export abstract class BaseStore extends EventEmitter<StoreEvents> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async computeInverseBloomFilter(expectedDiffSize?: number) {
|
||||||
|
// Get all keys.
|
||||||
|
const keys = await this.keys();
|
||||||
|
|
||||||
|
// Construct an Inverse Bloom Filter using the keys.
|
||||||
|
const ibf = InverseBloomFilter.from(keys, expectedDiffSize);
|
||||||
|
|
||||||
|
// Return the bloom filter.
|
||||||
|
return ibf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './base-store.js';
|
export * from './base-store.js';
|
||||||
export * from './store-inmemory.js';
|
export * from './store-inmemory.js';
|
||||||
export * from './store-inmemory-synced.js';
|
export * from './store-inmemory-synced.js';
|
||||||
|
export * from './store-remote-http.js';
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { BaseStore } from './base-store.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that accepts multiple stores and syncs them by key.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* Syncing is done by key - if values differ, then this will remain undetected.
|
||||||
|
* This is by design - local stores will sometimes prune irrelevant values as an optimization (while retaining the key).
|
||||||
|
* However, this cannot be done on the encrypted stores as the encrypted stores cannot inspect the data to know whether relevant/irrelevant.
|
||||||
|
* UUIDv7 keys are used to ensure uniqueness of keys.
|
||||||
|
*/
|
||||||
|
export class StoreComposite extends BaseStore {
|
||||||
|
static async from(localStore: BaseStore, remoteStores: Array<BaseStore>) {
|
||||||
|
const compositeStore = new this(localStore, remoteStores);
|
||||||
|
await compositeStore.sync();
|
||||||
|
return compositeStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private localStore: BaseStore,
|
||||||
|
private remoteStores: Array<BaseStore>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Local store events propagate to remote stores
|
||||||
|
this.localStore.on('set', async (payload) => {
|
||||||
|
// Propagate to all remote stores in parallel
|
||||||
|
await Promise.all(
|
||||||
|
this.remoteStores.map((remoteStore) =>
|
||||||
|
remoteStore.set(payload.key, payload.value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Local store events propagate to remote stores
|
||||||
|
this.localStore.on('delete', async (payload) => {
|
||||||
|
// Propagate to all remote stores in parallel
|
||||||
|
await Promise.all(
|
||||||
|
this.remoteStores.map((remoteStore) => remoteStore.delete(payload.key)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Local store events propagate to remote stores
|
||||||
|
this.localStore.on('clear', async () => {
|
||||||
|
// Propagate to all remote stores in parallel
|
||||||
|
await Promise.all(
|
||||||
|
this.remoteStores.map((remoteStore) => remoteStore.clear()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remote store events propagate to local store
|
||||||
|
for (const remoteStore of this.remoteStores) {
|
||||||
|
remoteStore.on('set', async (payload) => {
|
||||||
|
// Skip if the local store already has the key
|
||||||
|
if (await this.localStore.has(payload.key)) return;
|
||||||
|
|
||||||
|
await this.localStore.set(payload.key, payload.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
remoteStore.on('delete', async (payload) => {
|
||||||
|
if (await this.localStore.has(payload.key)) {
|
||||||
|
await this.localStore.delete(payload.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async all<T>() {
|
||||||
|
return await this.localStore.all<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
async keys() {
|
||||||
|
return await this.localStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(key: string) {
|
||||||
|
return await this.localStore.get<T>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T) {
|
||||||
|
await this.localStore.set(key, value);
|
||||||
|
this.emit('set', { key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
await this.localStore.delete(key);
|
||||||
|
this.emit('delete', { key });
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
await this.localStore.clear();
|
||||||
|
this.emit('clear', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async has(key: string) {
|
||||||
|
return await this.localStore.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
await Promise.all([
|
||||||
|
this.localStore.close(),
|
||||||
|
...this.remoteStores.map((store) => store.close()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync(): Promise<void> {
|
||||||
|
// Sync all remote stores in parallel
|
||||||
|
await Promise.all(
|
||||||
|
this.remoteStores.map(async (remoteStore) => {
|
||||||
|
// TODO: We want to negotiate using an Inverse Bloom Filter first to optimize the sync.
|
||||||
|
// Currently, this is doing a naive set comparison.
|
||||||
|
|
||||||
|
// Get all keys from the remote store
|
||||||
|
const remoteKeys = await remoteStore.keys();
|
||||||
|
const localKeys = await this.localStore.keys();
|
||||||
|
|
||||||
|
// Find keys missing in local store
|
||||||
|
const missingLocalKeys = remoteKeys.filter(
|
||||||
|
(key) => !localKeys.includes(key),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find keys missing in remote store
|
||||||
|
const missingRemoteKeys = localKeys.filter(
|
||||||
|
(key) => !remoteKeys.includes(key),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync missing keys from remote to local (in parallel)
|
||||||
|
await Promise.all(
|
||||||
|
missingLocalKeys.map(async (key) => {
|
||||||
|
const value = await remoteStore.get(key);
|
||||||
|
await this.localStore.set(key, value);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync missing keys from local to remote (in parallel)
|
||||||
|
await Promise.all(
|
||||||
|
missingRemoteKeys.map(async (key) => {
|
||||||
|
const value = await this.localStore.get(key);
|
||||||
|
await remoteStore.set(key, value);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { BaseStore } from './base-store.js';
|
||||||
|
import { Encrypter, SSEClient } from '../utils';
|
||||||
|
import { Bytes, PrivateKey, PublicKey } from '@xocash/primitives';
|
||||||
|
import { pack, unpack } from 'msgpackr';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export class StoreRemoteHTTP extends BaseStore {
|
||||||
|
static async from(
|
||||||
|
baseURL: string,
|
||||||
|
privateKey: PrivateKey,
|
||||||
|
encrypter: Encrypter,
|
||||||
|
) {
|
||||||
|
const store = new this(baseURL, privateKey, encrypter);
|
||||||
|
|
||||||
|
await store.subscribeEvents();
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimizations/Caches.
|
||||||
|
protected publicKey: PublicKey;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected baseURL: string,
|
||||||
|
protected privateKey: PrivateKey,
|
||||||
|
protected encrypter: Encrypter,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Derive the public key and cache it to optimize performance.
|
||||||
|
this.publicKey = privateKey.derivePublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
async keys() {
|
||||||
|
const response = await this.makeRequest('keys');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(key: string) {
|
||||||
|
const response = await this.makeRequest('get', { key });
|
||||||
|
|
||||||
|
if (!response.value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the value using the encrypter
|
||||||
|
const decryptedValue = await this.encrypter.decrypt(
|
||||||
|
new Uint8Array(response.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse the decrypted MessagePack value
|
||||||
|
return unpack(decryptedValue) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T): Promise<void> {
|
||||||
|
// Pack the encoded value with MessagePack and encrypt it
|
||||||
|
const packedValue = pack(value);
|
||||||
|
const encryptedValue = await this.encrypter.encrypt(packedValue);
|
||||||
|
|
||||||
|
await this.makeRequest('set', {
|
||||||
|
key,
|
||||||
|
value: encryptedValue, // Convert Uint8Array to regular array for MessagePack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string): Promise<void> {
|
||||||
|
await this.makeRequest('delete', { key });
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeEvents() {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const signature = await StoreRemoteHTTP.signRequest(
|
||||||
|
this.privateKey.toHex(),
|
||||||
|
timestamp,
|
||||||
|
'events',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
'X-Public-Key': this.publicKey.toHex(),
|
||||||
|
'X-Signature': Bytes.fromBytes(signature).toHex(),
|
||||||
|
'X-Timestamp': timestamp.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sseClient = new SSEClient(`${this.baseURL}events`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
onMessage: (e) => {
|
||||||
|
// Validate the payload using Zod.
|
||||||
|
// TODO: Use msgpackr
|
||||||
|
const payload = StoreRemoteHTTPEventSchema.parse(JSON.parse(e.data));
|
||||||
|
|
||||||
|
// Emit the event (update) to any listeners.
|
||||||
|
this.emit(payload.action, {
|
||||||
|
key: payload.key,
|
||||||
|
value: payload.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await sseClient.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeRequest(endpoint: string, payload: object = {}) {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const signature = await StoreRemoteHTTP.signRequest(
|
||||||
|
this.privateKey.toHex(),
|
||||||
|
timestamp,
|
||||||
|
endpoint,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/msgpackr',
|
||||||
|
Accept: 'application/msgpackr',
|
||||||
|
'X-Public-Key': this.publicKey.toHex(),
|
||||||
|
'X-Signature': Bytes.fromBytes(signature).toHex(),
|
||||||
|
'X-Timestamp': timestamp.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: payload ? pack(payload) : pack({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the URL class to compile the full path.
|
||||||
|
// NOTE: We do this to cater for those situations where the base URL might contain a trailing slash.
|
||||||
|
// E.g. https://wallet-sync.xo.cash/store/ vs https://wallet-sync.xo.cash/store
|
||||||
|
// TODO: THis is broken and currently requires a trailing slash in the BaseURL!
|
||||||
|
const requestURL = new URL(endpoint, this.baseURL).toString();
|
||||||
|
|
||||||
|
const response = await fetch(requestURL, requestOptions);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBuffer = await response.arrayBuffer();
|
||||||
|
return unpack(new Uint8Array(responseBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async signRequest(
|
||||||
|
privateKeyHex: string,
|
||||||
|
timestamp: number,
|
||||||
|
endpoint: string,
|
||||||
|
payload?: object,
|
||||||
|
) {
|
||||||
|
const messageBin = pack([endpoint, timestamp, payload]);
|
||||||
|
|
||||||
|
return PrivateKey.fromHex(privateKeyHex).signMessageSchnorr(messageBin);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verifySignature(
|
||||||
|
publicKeyHex: string,
|
||||||
|
signatureHex: string,
|
||||||
|
timestamp: number,
|
||||||
|
endpoint: string,
|
||||||
|
payload?: object,
|
||||||
|
) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (Math.abs(now - timestamp) > 60) {
|
||||||
|
throw new Error(`Request has expired`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageBin = pack([endpoint, timestamp, payload]);
|
||||||
|
|
||||||
|
const isValidSignature = PublicKey.fromHex(
|
||||||
|
publicKeyHex,
|
||||||
|
).verifyMessageSchnorr(messageBin, signatureHex);
|
||||||
|
|
||||||
|
if (!isValidSignature) {
|
||||||
|
throw new Error(`The provided signature is invalid`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreRemoteHTTPEventSchema = z.object({
|
||||||
|
publicKey: z.string(),
|
||||||
|
action: z.enum(['set', 'delete', 'clear']),
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
timestamp: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// TODO: Consider whether we should shuffle this to Stack Util (as opposed to Primitive).
|
// TODO: Consider whether we should shuffle this to Stack Util (as opposed to Primitive).
|
||||||
// It is async which breaks from our Primitive patterns.
|
// It is async which breaks from our Primitive patterns.
|
||||||
|
|
||||||
import { utf8ToBin } from '@bitauth/libauth';
|
import { generateRandomBytes, utf8ToBin } from '@bitauth/libauth';
|
||||||
|
|
||||||
// NOTE: We need to provide a default salt otherwise we would be deriving a key per each operation.
|
// NOTE: We need to provide a default salt otherwise we would be deriving a key per each operation.
|
||||||
// This would be far too computationally expensive.
|
// This would be far too computationally expensive.
|
||||||
@@ -74,6 +74,12 @@ export class Encrypter {
|
|||||||
return this.fromBytes(key, opts);
|
return this.fromBytes(key, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async generateRandom(opts: Partial<EncrypterOpts> = {}) {
|
||||||
|
const key = generateRandomBytes(32);
|
||||||
|
|
||||||
|
return this.fromBytes(key, opts);
|
||||||
|
}
|
||||||
|
|
||||||
async encrypt(payload: Uint8Array | string): Promise<Uint8Array> {
|
async encrypt(payload: Uint8Array | string): Promise<Uint8Array> {
|
||||||
// Convert string payload to Uint8Array if needed
|
// Convert string payload to Uint8Array if needed
|
||||||
const data = typeof payload === 'string' ? utf8ToBin(payload) : payload;
|
const data = typeof payload === 'string' ? utf8ToBin(payload) : payload;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export * from './encrypter.js';
|
|||||||
export * from './event-emitter.js';
|
export * from './event-emitter.js';
|
||||||
export * from './ext-error.js';
|
export * from './ext-error.js';
|
||||||
export * from './hasher.js';
|
export * from './hasher.js';
|
||||||
|
export * from './inverse-bloom-filter.js';
|
||||||
export * from './memoizer.js';
|
export * from './memoizer.js';
|
||||||
export * from './misc.js';
|
export * from './misc.js';
|
||||||
|
export * from './sse-client.js';
|
||||||
export * from './uuid.js';
|
export * from './uuid.js';
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ interface Cell {
|
|||||||
hashSum: bigint;
|
hashSum: bigint;
|
||||||
}
|
}
|
||||||
|
|
||||||
class InverseBloomFilter {
|
export class InverseBloomFilter {
|
||||||
|
static from(items: string[], expectedDiffSize?: number) {
|
||||||
|
return new this(items, expectedDiffSize);
|
||||||
|
}
|
||||||
|
|
||||||
private size: number;
|
private size: number;
|
||||||
private cells: Cell[];
|
private cells: Cell[];
|
||||||
private items: Set<string>;
|
private items: Set<string>;
|
||||||
@@ -36,6 +40,41 @@ class InverseBloomFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findMaybeMissing(otherFilterBytes: Uint8Array): string[] {
|
||||||
|
const diffCells = this.subtractFilter(otherFilterBytes);
|
||||||
|
const missing: string[] = [];
|
||||||
|
|
||||||
|
for (const item of this.items) {
|
||||||
|
const idx = this.hashToIndex(item);
|
||||||
|
const itemId = this.hashToId(item);
|
||||||
|
const cell = diffCells[idx];
|
||||||
|
|
||||||
|
if (cell.count === 1 && cell.idSum === itemId) {
|
||||||
|
missing.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
toBytes(): Uint8Array {
|
||||||
|
const cellSize = 4 + 8 + 8;
|
||||||
|
const bytes = new Uint8Array(8 + this.size * cellSize);
|
||||||
|
const view = new DataView(bytes.buffer);
|
||||||
|
|
||||||
|
view.setBigInt64(0, BigInt(this.size), true);
|
||||||
|
|
||||||
|
let offset = 8;
|
||||||
|
for (const cell of this.cells) {
|
||||||
|
view.setInt32(offset, cell.count, true);
|
||||||
|
view.setBigInt64(offset + 4, cell.idSum, true);
|
||||||
|
view.setBigInt64(offset + 12, cell.hashSum, true);
|
||||||
|
offset += cellSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
private hashToIndex(value: string): number {
|
private hashToIndex(value: string): number {
|
||||||
let hash = 2166136261;
|
let hash = 2166136261;
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
@@ -68,24 +107,6 @@ class InverseBloomFilter {
|
|||||||
cell.hashSum ^= itemId;
|
cell.hashSum ^= itemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
toBytes(): Uint8Array {
|
|
||||||
const cellSize = 4 + 8 + 8;
|
|
||||||
const bytes = new Uint8Array(8 + this.size * cellSize);
|
|
||||||
const view = new DataView(bytes.buffer);
|
|
||||||
|
|
||||||
view.setBigInt64(0, BigInt(this.size), true);
|
|
||||||
|
|
||||||
let offset = 8;
|
|
||||||
for (const cell of this.cells) {
|
|
||||||
view.setInt32(offset, cell.count, true);
|
|
||||||
view.setBigInt64(offset + 4, cell.idSum, true);
|
|
||||||
view.setBigInt64(offset + 12, cell.hashSum, true);
|
|
||||||
offset += cellSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private subtractFilter(otherBytes: Uint8Array): Cell[] {
|
private subtractFilter(otherBytes: Uint8Array): Cell[] {
|
||||||
const view = new DataView(otherBytes.buffer);
|
const view = new DataView(otherBytes.buffer);
|
||||||
const otherSize = Number(view.getBigInt64(0, true));
|
const otherSize = Number(view.getBigInt64(0, true));
|
||||||
@@ -114,21 +135,4 @@ class InverseBloomFilter {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
findMaybeMissing(otherFilterBytes: Uint8Array): string[] {
|
|
||||||
const diffCells = this.subtractFilter(otherFilterBytes);
|
|
||||||
const missing: string[] = [];
|
|
||||||
|
|
||||||
for (const item of this.items) {
|
|
||||||
const idx = this.hashToIndex(item);
|
|
||||||
const itemId = this.hashToId(item);
|
|
||||||
const cell = diffCells[idx];
|
|
||||||
|
|
||||||
if (cell.count === 1 && cell.idSum === itemId) {
|
|
||||||
missing.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return missing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,205 +1,286 @@
|
|||||||
// TODO: This probably needs some work.
|
// TODO: 1. Handle reconnection logic.
|
||||||
// The reason this exists is that Browser's native EventSource is rubbish:
|
// 2. Allow specifying custom
|
||||||
// 1. It does not allow passing of headers.
|
// 3. Try to make it so that connect DOES NOT resolve until connection is actually establish.
|
||||||
// 2. It is browser-only (this implementation aims to work for Node also)
|
// There's a very slim chance of a race-condition otherwise where we might miss an event.
|
||||||
// 3. It does not handle disconnects/reconnects gracefully
|
// Note that this IS NOT trivial. Claude will probably NOT be able to do this for you!
|
||||||
// 4. It does not handle last-event-id (important for history).
|
|
||||||
// 5. It does not allow a custom fetch function (probably not necessary, but there's a reason I'd rather support this which I'll explain some other time).
|
|
||||||
// This might eventually be shifted out of XO/stack package and into its own.
|
|
||||||
// Q: "Why not just leverage an existing package?"
|
|
||||||
// A: They're all bad - the "go to" is a Microsoft (@azure/fetch-event-soruce) that is no longer maintained.
|
|
||||||
// It does not support NodeJS (or Workers) and we cannot depend upon forks of it.
|
|
||||||
|
|
||||||
export interface SSEOptions {
|
|
||||||
fetch?: typeof fetch;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
onMessage?: (event: SSEMessageEvent) => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
onOpen?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
maxRetries?: number;
|
|
||||||
retryDelay?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSEMessageEvent {
|
|
||||||
data: string;
|
|
||||||
type: string;
|
|
||||||
id: string | null;
|
|
||||||
lastEventId: string;
|
|
||||||
origin: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSEEvent {
|
|
||||||
data: string;
|
|
||||||
event: string;
|
|
||||||
id: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Server-Sent Events client implementation using fetch API.
|
||||||
|
* Supports custom headers, POST requests, and is non-blocking.
|
||||||
|
*/
|
||||||
export class SSEClient {
|
export class SSEClient {
|
||||||
private url: string;
|
/**
|
||||||
private options: Required<Omit<SSEOptions, 'headers'>> & {
|
* Creates and connects a new SSEClient instance.
|
||||||
headers: Record<string, string>;
|
* @param url The URL to connect to
|
||||||
};
|
* @param options Configuration options
|
||||||
private abortController: AbortController | null;
|
* @returns A new connected SSEClient instance
|
||||||
private retryCount: number;
|
*/
|
||||||
private lastEventId: string;
|
public static async from(
|
||||||
private connected: boolean;
|
url: string,
|
||||||
|
options: SSEClientOptions,
|
||||||
|
): Promise<SSEClient> {
|
||||||
|
const client = new SSEClient(url, options);
|
||||||
|
await client.connect();
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(url: string, options: SSEOptions = {}) {
|
// State.
|
||||||
|
private url: string;
|
||||||
|
private options: SSEClientOptions;
|
||||||
|
private controller: AbortController;
|
||||||
|
private connected: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SSEClient instance.
|
||||||
|
* @param url The URL to connect to
|
||||||
|
* @param options Configuration options
|
||||||
|
*/
|
||||||
|
constructor(url: string, options: SSEClientOptions) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.options = {
|
this.options = {
|
||||||
fetch: globalThis.fetch,
|
method: 'GET',
|
||||||
headers: {},
|
headers: {
|
||||||
onMessage: () => {},
|
Accept: 'text/event-stream',
|
||||||
onError: () => {},
|
'Cache-Control': 'no-cache',
|
||||||
|
},
|
||||||
onOpen: () => {},
|
onOpen: () => {},
|
||||||
|
onMessage: () => {},
|
||||||
|
onError: (error) => console.error('SSEClient error:', error),
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
maxRetries: 5,
|
|
||||||
retryDelay: 3000,
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
this.controller = new AbortController();
|
||||||
this.abortController = null;
|
|
||||||
this.retryCount = 0;
|
|
||||||
this.lastEventId = '';
|
|
||||||
this.connected = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the SSE endpoint.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* This currently does not await properly and has a chance of introducing a race-condition.
|
||||||
|
* We want to wait for the connection to ESTABLISH before resolving.
|
||||||
|
* Otherwise, there is a slight possibility of missing events during the connection phase.
|
||||||
|
* If you try to fix this, be cautious about using a while(true) loop: JS is single threaded and you may block.
|
||||||
|
* Try to use the Streams event features instead.
|
||||||
|
*/
|
||||||
public async connect(): Promise<void> {
|
public async connect(): Promise<void> {
|
||||||
if (this.connected) {
|
if (this.connected) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.abortController = new AbortController();
|
this.connected = true;
|
||||||
|
const { method, headers, body } = this.options;
|
||||||
|
|
||||||
try {
|
const fetchOptions: RequestInit = {
|
||||||
const response = await this.options.fetch(this.url, {
|
method,
|
||||||
headers: {
|
headers,
|
||||||
Accept: 'text/event-stream',
|
body,
|
||||||
'Cache-Control': 'no-cache',
|
signal: this.controller.signal,
|
||||||
'Last-Event-ID': this.lastEventId,
|
cache: 'no-store',
|
||||||
...this.options.headers,
|
|
||||||
},
|
|
||||||
signal: this.abortController.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.body) {
|
|
||||||
throw new Error('ReadableStream not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connected = true;
|
|
||||||
this.options.onOpen();
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
this.handleDisconnect();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
|
||||||
const lines = buffer.split(/\r\n|\r|\n/);
|
|
||||||
|
|
||||||
buffer = this.processEventStream(lines);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.options.onError(
|
|
||||||
error instanceof Error ? error : new Error(String(error)),
|
|
||||||
);
|
|
||||||
this.handleDisconnect();
|
|
||||||
|
|
||||||
if (this.retryCount < this.options.maxRetries) {
|
|
||||||
this.retryCount++;
|
|
||||||
setTimeout(() => this.connect(), this.options.retryDelay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private processEventStream(lines: string[]): string {
|
|
||||||
let event: SSEEvent = {
|
|
||||||
data: '',
|
|
||||||
event: 'message',
|
|
||||||
id: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < lines.length - 1; i++) {
|
// Using an async IIFE to handle the connection without blocking
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// TODO: We want to resolve only AFTER we handle connection.
|
||||||
|
// But this is not trivial!
|
||||||
|
const response = await fetch(this.url, fetchOptions);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Response body is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify that the connection has been established
|
||||||
|
if (this.options.onOpen) {
|
||||||
|
this.options.onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
this.connected = false;
|
||||||
|
if (this.options.onClose) {
|
||||||
|
this.options.onClose();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the received chunk and add it to the buffer
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
// Process any complete events in the buffer
|
||||||
|
const events = this.parseEvents(buffer);
|
||||||
|
buffer = events.remainder;
|
||||||
|
|
||||||
|
// Handle each complete event
|
||||||
|
for (const event of events.parsed) {
|
||||||
|
if (this.options.onMessage) {
|
||||||
|
this.options.onMessage(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.connected = false;
|
||||||
|
|
||||||
|
if (this.options.onError) {
|
||||||
|
this.options.onError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.onClose) {
|
||||||
|
this.options.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses SSE events from a text buffer.
|
||||||
|
* @param buffer The buffer to parse
|
||||||
|
* @returns Parsed events and the remaining buffer
|
||||||
|
*/
|
||||||
|
private parseEvents(buffer: string): {
|
||||||
|
parsed: SSEvent[];
|
||||||
|
remainder: string;
|
||||||
|
} {
|
||||||
|
const events: SSEvent[] = [];
|
||||||
|
const lines = buffer.split(/\r\n|\r|\n/);
|
||||||
|
|
||||||
|
let currentEvent: Partial<SSEvent> = {};
|
||||||
|
let completeEventCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
if (line.length === 0) {
|
// Empty line signals the end of an event
|
||||||
// Empty line means dispatch the event
|
if (line === '') {
|
||||||
if (event.data.length > 0) {
|
if (currentEvent.data) {
|
||||||
this.dispatchEvent(event);
|
// Remove trailing newline if present
|
||||||
event = {
|
currentEvent.data = currentEvent.data.replace(/\n$/, '');
|
||||||
data: '',
|
events.push(currentEvent as SSEvent);
|
||||||
event: 'message',
|
currentEvent = {};
|
||||||
id: null,
|
completeEventCount = i + 1;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [fieldName, ...fieldValue] = line.split(':');
|
// Parse field: value format
|
||||||
const value = fieldValue.join(':').trim();
|
const colonIndex = line.indexOf(':');
|
||||||
|
if (colonIndex === -1) continue;
|
||||||
|
|
||||||
switch (fieldName) {
|
const field = line.slice(0, colonIndex);
|
||||||
case 'event':
|
// Skip initial space after colon if present
|
||||||
event.event = value;
|
const valueStartIndex =
|
||||||
break;
|
colonIndex + 1 + (line[colonIndex + 1] === ' ' ? 1 : 0);
|
||||||
case 'data':
|
const value = line.slice(valueStartIndex);
|
||||||
event.data += value + '\n';
|
|
||||||
break;
|
if (field === 'data') {
|
||||||
case 'id':
|
currentEvent.data = currentEvent.data
|
||||||
event.id = value;
|
? currentEvent.data + '\n' + value
|
||||||
this.lastEventId = value;
|
: value;
|
||||||
break;
|
} else if (field === 'event') {
|
||||||
case 'retry': {
|
currentEvent.event = value;
|
||||||
const retryInt = parseInt(value, 10);
|
} else if (field === 'id') {
|
||||||
if (!isNaN(retryInt)) {
|
currentEvent.id = value;
|
||||||
this.options.retryDelay = retryInt;
|
} else if (field === 'retry') {
|
||||||
}
|
const retryMs = parseInt(value, 10);
|
||||||
break;
|
if (!isNaN(retryMs)) {
|
||||||
|
currentEvent.retry = retryMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the last incomplete line as the new buffer
|
const remainder = lines.slice(completeEventCount).join('\n');
|
||||||
return lines[lines.length - 1];
|
return { parsed: events, remainder };
|
||||||
}
|
|
||||||
|
|
||||||
private dispatchEvent(event: SSEEvent): void {
|
|
||||||
const messageEvent: SSEMessageEvent = {
|
|
||||||
data: event.data.slice(0, -1), // Remove trailing newline
|
|
||||||
type: event.event,
|
|
||||||
id: event.id,
|
|
||||||
lastEventId: this.lastEventId,
|
|
||||||
origin: this.url,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.options.onMessage(messageEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDisconnect(): void {
|
|
||||||
this.connected = false;
|
|
||||||
this.options.onClose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the SSE connection.
|
||||||
|
*/
|
||||||
public close(): void {
|
public close(): void {
|
||||||
if (this.abortController) {
|
if (!this.connected) return;
|
||||||
this.abortController.abort();
|
|
||||||
this.handleDisconnect();
|
this.controller.abort();
|
||||||
}
|
this.connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the client is currently connected.
|
||||||
|
* @returns Whether the client is connected
|
||||||
|
*/
|
||||||
|
public isConnected(): boolean {
|
||||||
|
return this.connected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the SSEClient.
|
||||||
|
*/
|
||||||
|
export interface SSEClientOptions {
|
||||||
|
/**
|
||||||
|
* HTTP method to use (GET or POST).
|
||||||
|
*/
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP headers to send with the request.
|
||||||
|
*/
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body to send with POST requests.
|
||||||
|
*/
|
||||||
|
body?: string | FormData | URLSearchParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the connection is established.
|
||||||
|
*/
|
||||||
|
onOpen?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a message is received.
|
||||||
|
*/
|
||||||
|
onMessage?: (event: SSEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an error occurs.
|
||||||
|
*/
|
||||||
|
onError?: (error: unknown) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the connection is closed.
|
||||||
|
*/
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Server-Sent Event.
|
||||||
|
*/
|
||||||
|
export interface SSEvent {
|
||||||
|
/**
|
||||||
|
* Event data.
|
||||||
|
*/
|
||||||
|
data: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event type.
|
||||||
|
*/
|
||||||
|
event?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event ID.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnection time in milliseconds.
|
||||||
|
*/
|
||||||
|
retry?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,51 @@ export type PolicyFunction<T = any> = (
|
|||||||
newActivity: Activity<string, T>,
|
newActivity: Activity<string, T>,
|
||||||
) => Activity<string, T>;
|
) => Activity<string, T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements an event sourcing pattern for managing application state.
|
||||||
|
*
|
||||||
|
* Event sourcing is an architectural pattern where:
|
||||||
|
* - All changes to application state are stored as a sequence of immutable events
|
||||||
|
* - The current state is derived by replaying these events in chronological order
|
||||||
|
* - Events serve as both the source of truth and a complete audit log
|
||||||
|
*
|
||||||
|
* This implementation provides:
|
||||||
|
* - Eventually consistent state across distributed systems
|
||||||
|
* - Conflict resolution through configurable policies
|
||||||
|
* - Point-in-time state reconstruction capability
|
||||||
|
* - Separation between event storage and derived state (snapshot)
|
||||||
|
*
|
||||||
|
* Events (Activities) in this system:
|
||||||
|
* - Have a unique identifier
|
||||||
|
* - Include a timestamp for ordering
|
||||||
|
* - Contain a type and associated value
|
||||||
|
* - Are processed according to registered type-specific policies
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Create Activities manager with a store implementation
|
||||||
|
* const activities = new Activities(new MyBaseStore());
|
||||||
|
*
|
||||||
|
* // Register a custom policy for wallet name conflicts
|
||||||
|
* activities.registerPolicy('WalletName', (existing, newActivity) => {
|
||||||
|
* // Keep the most recent wallet name
|
||||||
|
* return existing?.timestamp! > newActivity.timestamp! ? existing : newActivity;
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Initialize by compiling snapshot from stored events
|
||||||
|
* await activities.compileSnapshot();
|
||||||
|
*
|
||||||
|
* // Add a new activity
|
||||||
|
* await activities.addActivity({
|
||||||
|
* type: 'WalletName',
|
||||||
|
* id: 'wallet123',
|
||||||
|
* value: 'My Primary Wallet'
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @template SnapshotByType - Structured representation of the current system state
|
||||||
|
* @template PolicyFunction - Function type for conflict resolution strategies
|
||||||
|
*/
|
||||||
export class Activities {
|
export class Activities {
|
||||||
// TODO: Consider splitting into its own mutable class: ActivitySnapshot.
|
// TODO: Consider splitting into its own mutable class: ActivitySnapshot.
|
||||||
// This might be cleaner as we'll likely want to be able to view a raw timeline of activities.
|
// This might be cleaner as we'll likely want to be able to view a raw timeline of activities.
|
||||||
@@ -110,6 +155,10 @@ export class Activities {
|
|||||||
storeKey: string,
|
storeKey: string,
|
||||||
activity: Activity<string, any>,
|
activity: Activity<string, any>,
|
||||||
) {
|
) {
|
||||||
|
if (!activity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!activity.timestamp) {
|
if (!activity.timestamp) {
|
||||||
console.warn('Activity without timestamp encountered:', storeKey);
|
console.warn('Activity without timestamp encountered:', storeKey);
|
||||||
return;
|
return;
|
||||||
@@ -132,12 +181,12 @@ export class Activities {
|
|||||||
const oldStoreKey = this.storeKeyMap.get(snapshotKey);
|
const oldStoreKey = this.storeKeyMap.get(snapshotKey);
|
||||||
if (oldStoreKey) {
|
if (oldStoreKey) {
|
||||||
// Delete the old activity from the store
|
// Delete the old activity from the store
|
||||||
await this.store.delete(oldStoreKey);
|
await this.store.set(oldStoreKey, null);
|
||||||
}
|
}
|
||||||
this.storeKeyMap.set(snapshotKey, storeKey);
|
this.storeKeyMap.set(snapshotKey, storeKey);
|
||||||
} else {
|
} else {
|
||||||
// If we kept the existing activity, delete the new activity from the store
|
// If we kept the existing activity, delete the new activity from the store
|
||||||
await this.store.delete(storeKey);
|
await this.store.set(storeKey, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +216,11 @@ export class Activities {
|
|||||||
const activities = await this.store.all<Activity<string, any>>();
|
const activities = await this.store.all<Activity<string, any>>();
|
||||||
const entries = Object.entries(activities);
|
const entries = Object.entries(activities);
|
||||||
|
|
||||||
|
// Filter for null payloads
|
||||||
|
const filteredEntries = entries.filter(([_, payload]) => payload);
|
||||||
|
|
||||||
// Sort activities by timestamp using UUID v7's embedded timestamp
|
// Sort activities by timestamp using UUID v7's embedded timestamp
|
||||||
const activitiesSorted = entries.sort(([keyA], [keyB]) => {
|
const activitiesSorted = filteredEntries.sort(([keyA], [keyB]) => {
|
||||||
const timestampA = getTimestampFromUUID(keyA);
|
const timestampA = getTimestampFromUUID(keyA);
|
||||||
const timestampB = getTimestampFromUUID(keyB);
|
const timestampB = getTimestampFromUUID(keyB);
|
||||||
return Number(timestampA - timestampB);
|
return Number(timestampA - timestampB);
|
||||||
|
|||||||
@@ -145,4 +145,19 @@ export abstract class BaseWallet<
|
|||||||
abstract addActivity<T>(activity: T): Promise<void>;
|
abstract addActivity<T>(activity: T): Promise<void>;
|
||||||
|
|
||||||
abstract export(): Promise<string>;
|
abstract export(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID for this wallet.
|
||||||
|
*/
|
||||||
|
abstract getId(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the encryption key to use for this wallet.
|
||||||
|
*/
|
||||||
|
abstract getEncryptionKey(): Promise<Uint8Array>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the signing key to use for this wallet.
|
||||||
|
*/
|
||||||
|
abstract getSigningKey(): Promise<Uint8Array>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
encodeExtendedJson,
|
encodeExtendedJson,
|
||||||
} from '@xocash/primitives';
|
} from '@xocash/primitives';
|
||||||
|
|
||||||
import { hexToBin } from '@bitauth/libauth';
|
import { hash256, binToHex, hexToBin, utf8ToBin } from '@bitauth/libauth';
|
||||||
|
|
||||||
export const DERIVATION_PATH = "m/44'/145'/0'";
|
export const DERIVATION_PATH = "m/44'/145'/0'";
|
||||||
export const ADDRESS_GAP = 20;
|
export const ADDRESS_GAP = 20;
|
||||||
@@ -91,23 +91,6 @@ export class WalletHD<
|
|||||||
// Store child wallets, grouped by chain ("change") path in BIP44 spec.
|
// Store child wallets, grouped by chain ("change") path in BIP44 spec.
|
||||||
public wallets: { [chainPath: number]: Array<WalletType> } = {};
|
public wallets: { [chainPath: number]: Array<WalletType> } = {};
|
||||||
|
|
||||||
constructor(
|
|
||||||
public blockchain: BaseBlockchain,
|
|
||||||
public store: BaseStore,
|
|
||||||
public cache: BaseStore,
|
|
||||||
public hdPrivateNode: HDPrivateNode,
|
|
||||||
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
// Initialize empty arrays for our wallets.
|
|
||||||
this.wallets[CHAIN_EXTERNAL] = [];
|
|
||||||
this.wallets[CHAIN_INTERNAL] = [];
|
|
||||||
|
|
||||||
// Initialize proxies to our stored data.
|
|
||||||
this.genesisData = this.store.proxy('genesis');
|
|
||||||
}
|
|
||||||
|
|
||||||
static async from<T extends typeof WalletHD>(
|
static async from<T extends typeof WalletHD>(
|
||||||
this: T,
|
this: T,
|
||||||
services: WalletServices,
|
services: WalletServices,
|
||||||
@@ -166,6 +149,46 @@ export class WalletHD<
|
|||||||
) as InstanceType<T>;
|
) as InstanceType<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public blockchain: BaseBlockchain,
|
||||||
|
public store: BaseStore,
|
||||||
|
public cache: BaseStore,
|
||||||
|
public hdPrivateNode: HDPrivateNode,
|
||||||
|
private walletFactory: WalletP2PKHFactory<WalletType> = useWalletP2PKH,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Initialize empty arrays for our wallets.
|
||||||
|
this.wallets[CHAIN_EXTERNAL] = [];
|
||||||
|
this.wallets[CHAIN_INTERNAL] = [];
|
||||||
|
|
||||||
|
// Initialize proxies to our stored data.
|
||||||
|
this.genesisData = this.store.proxy('genesis');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getId() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
|
return binToHex(
|
||||||
|
await this.cache.getOrSet('walletId', async () =>
|
||||||
|
this.deriveKey('walletId'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEncryptionKey() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
|
return await this.cache.getOrSet('encryptionKey', async () =>
|
||||||
|
this.deriveKey('encryptionKey'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSigningKey() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
|
return this.cache.getOrSet('signingKey', async () =>
|
||||||
|
this.deriveKey('signingKey'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
// Get the genesis data.
|
// Get the genesis data.
|
||||||
const genesisData = await this.genesisData.getOrFail();
|
const genesisData = await this.genesisData.getOrFail();
|
||||||
@@ -504,6 +527,19 @@ export class WalletHD<
|
|||||||
|
|
||||||
this.emit('stateUpdated', null);
|
this.emit('stateUpdated', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async deriveKey(keyName: string): Promise<Uint8Array> {
|
||||||
|
// Get the genesis data for this wallet.
|
||||||
|
// TODO: This needs to change!
|
||||||
|
const genesisData = await this.store.getOrFail('genesis');
|
||||||
|
|
||||||
|
// Hash the genesis data using Hash256 and the key ("WalletID") for this derivation using Hash256.
|
||||||
|
const genesisDataHashed = hash256(utf8ToBin(JSON.stringify(genesisData)));
|
||||||
|
const keyHashed = hash256(utf8ToBin(keyName));
|
||||||
|
|
||||||
|
// Combine the hashes and do one final hash for good measure.
|
||||||
|
return hash256(new Uint8Array([...genesisDataHashed, ...keyHashed]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
type Input,
|
type Input,
|
||||||
binToHex,
|
binToHex,
|
||||||
|
hash256,
|
||||||
sha256,
|
sha256,
|
||||||
utf8ToBin,
|
utf8ToBin,
|
||||||
walletTemplateP2pkhNonHd,
|
walletTemplateP2pkhNonHd,
|
||||||
@@ -54,19 +55,6 @@ export class WalletP2PKH extends BaseWallet<WalletP2PKHEvents> {
|
|||||||
public isStarted = false;
|
public isStarted = false;
|
||||||
public status: string | null = null;
|
public status: string | null = null;
|
||||||
|
|
||||||
constructor(
|
|
||||||
// Dependencies.
|
|
||||||
public blockchain: BaseBlockchain,
|
|
||||||
// Persistent Storage.
|
|
||||||
public store: BaseStore,
|
|
||||||
// Cache.
|
|
||||||
public cache: BaseStore,
|
|
||||||
// Properties.
|
|
||||||
public privateKey: PrivateKey,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async from<T extends typeof WalletP2PKH>(
|
static async from<T extends typeof WalletP2PKH>(
|
||||||
this: T,
|
this: T,
|
||||||
services: WalletServices,
|
services: WalletServices,
|
||||||
@@ -170,10 +158,40 @@ export class WalletP2PKH extends BaseWallet<WalletP2PKHEvents> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getId() {
|
constructor(
|
||||||
const genesisData = await this.store.getOrFail('genesis');
|
// Dependencies.
|
||||||
|
public blockchain: BaseBlockchain,
|
||||||
|
// Persistent Storage.
|
||||||
|
public store: BaseStore,
|
||||||
|
// Cache.
|
||||||
|
public cache: BaseStore,
|
||||||
|
// Properties.
|
||||||
|
public privateKey: PrivateKey,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
return sha256.hash(utf8ToBin(JSON.stringify(genesisData)));
|
async getId() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
|
return binToHex(
|
||||||
|
await this.cache.getOrSet('walletId', async () =>
|
||||||
|
this.deriveKey('walletId'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEncryptionKey() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
|
return await this.cache.getOrSet('encryptionKey', async () =>
|
||||||
|
this.deriveKey('encryptionKey'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSigningKey() {
|
||||||
|
// NOTE: Use the cache to optimize future calls.
|
||||||
|
return this.cache.getOrSet('signingKey', async () =>
|
||||||
|
this.deriveKey('signingKey'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
@@ -431,6 +449,19 @@ export class WalletP2PKH extends BaseWallet<WalletP2PKHEvents> {
|
|||||||
// Store the data.
|
// Store the data.
|
||||||
await this.store.set<T>(activityId, activity);
|
await this.store.set<T>(activityId, activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async deriveKey(keyName: string): Promise<Uint8Array> {
|
||||||
|
// Get the genesis data for this wallet.
|
||||||
|
// TODO: This needs to change!
|
||||||
|
const genesisData = await this.store.getOrFail('genesis');
|
||||||
|
|
||||||
|
// Hash the genesis data using Hash256 and the key ("WalletID") for this derivation using Hash256.
|
||||||
|
const genesisDataHashed = hash256(utf8ToBin(JSON.stringify(genesisData)));
|
||||||
|
const keyHashed = hash256(utf8ToBin(keyName));
|
||||||
|
|
||||||
|
// Combine the hashes and do one final hash for good measure.
|
||||||
|
return hash256(new Uint8Array([...genesisDataHashed, ...keyHashed]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
import { StoreRemoteHTTP } from '~/src/storage/store-remote-http.js';
|
||||||
|
import { Encrypter } from '~/src/utils/encrypter';
|
||||||
|
import { PrivateKey } from '@xocash/primitives';
|
||||||
|
|
||||||
|
describe('StoreRemoteHTTP', () => {
|
||||||
|
describe('test', () => {
|
||||||
|
it('test', async () => {
|
||||||
|
const privateKey = PrivateKey.generateRandom();
|
||||||
|
const encrypter = await Encrypter.generateRandom();
|
||||||
|
const storeHTTP = await StoreRemoteHTTP.from(
|
||||||
|
'http://localhost:3000/store/',
|
||||||
|
privateKey,
|
||||||
|
encrypter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// const keys = await storeHTTP.keys();
|
||||||
|
|
||||||
|
// console.log(keys);
|
||||||
|
|
||||||
|
await storeHTTP.set('someValue', { someThing: 'asd' });
|
||||||
|
await storeHTTP.set('someOtherValue', { someThing: 'asd' });
|
||||||
|
|
||||||
|
console.error(await storeHTTP.keys());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
@@ -249,6 +249,7 @@ describe('Activities', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
test('should cleanup old activities from store when newer versions are added', async () => {
|
test('should cleanup old activities from store when newer versions are added', async () => {
|
||||||
// Add first activity
|
// Add first activity
|
||||||
await activities.addActivity({
|
await activities.addActivity({
|
||||||
@@ -282,4 +283,5 @@ describe('Activities', () => {
|
|||||||
// Should now have 2 activities
|
// Should now have 2 activities
|
||||||
expect(Object.keys(await store.all()).length).toBe(2);
|
expect(Object.keys(await store.all()).length).toBe(2);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user