@mblaney/holster 1.0.35 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/build/holster.js +3 -3
  2. package/build/holster.js.map +3 -3
  3. package/dist/holster.js +4 -0
  4. package/dist/holster.js.map +7 -0
  5. package/package.json +22 -5
  6. package/src/dup.js +4 -5
  7. package/src/get.js +10 -19
  8. package/src/ham.js +22 -49
  9. package/src/holster.js +172 -58
  10. package/src/index.js +1 -1
  11. package/src/radisk.js +89 -86
  12. package/src/sea-utils.js +1 -1
  13. package/src/sea.js +0 -52
  14. package/src/store.js +8 -8
  15. package/src/user.js +0 -4
  16. package/src/wire.js +209 -79
  17. package/ts/src/array.ts +78 -0
  18. package/ts/src/buffer.ts +441 -0
  19. package/ts/src/dup.ts +38 -0
  20. package/ts/src/get.ts +83 -0
  21. package/ts/src/ham.ts +258 -0
  22. package/ts/src/holster.ts +968 -0
  23. package/ts/src/index.ts +35 -0
  24. package/ts/src/radisk.ts +591 -0
  25. package/ts/src/radix.ts +157 -0
  26. package/ts/src/schemas.ts +515 -0
  27. package/ts/src/sea-utils.ts +103 -0
  28. package/ts/src/sea.ts +388 -0
  29. package/ts/src/store.ts +321 -0
  30. package/ts/src/user.ts +377 -0
  31. package/ts/src/utils.ts +232 -0
  32. package/ts/src/wire.ts +961 -0
  33. package/.dockerignore +0 -9
  34. package/.github/workflows/node.js.yml +0 -31
  35. package/.github/workflows/npm-publish.yml +0 -33
  36. package/Dockerfile +0 -20
  37. package/examples/index.html +0 -33
  38. package/examples/index.js +0 -49
  39. package/examples/user.html +0 -31
  40. package/examples/user.js +0 -93
  41. package/examples/wire.html +0 -27
  42. package/examples/wire.js +0 -55
  43. package/test/holster.get.test.js +0 -94
  44. package/test/holster.lex.test.js +0 -196
  45. package/test/holster.off.test.js +0 -53
  46. package/test/holster.on.test.js +0 -264
  47. package/test/holster.put.test.js +0 -358
  48. package/test/holster.secure.test.js +0 -202
  49. package/test/holster.user.get.test.js +0 -114
  50. package/test/holster.user.lex.test.js +0 -149
  51. package/test/holster.user.off.test.js +0 -73
  52. package/test/holster.user.on.test.js +0 -300
  53. package/test/holster.user.put.test.js +0 -463
  54. package/test/kitty/! +0 -9
  55. package/test/names.js +0 -225
  56. package/test/radisk.cache.test.js +0 -213
  57. package/test/radisk.test.js +0 -331
  58. package/test/radix.test.js +0 -117
  59. package/test/sea.test.js +0 -284
  60. package/test/split.test.js +0 -317
  61. package/test/store.test.js +0 -316
  62. package/test/system/basic.test.js +0 -29
  63. package/test/system/chained-on.test.js +0 -41
  64. package/test/system/concurrent-listeners.test.js +0 -125
  65. package/test/system/immediate-read.test.js +0 -30
  66. package/test/system/listener-before-data.test.js +0 -51
  67. package/test/system/promise-wrapper.test.js +0 -47
  68. package/test/system/user-chained-on.test.js +0 -60
  69. package/test/system/user-concurrent-listeners.test.js +0 -142
  70. package/test/system/user-listener-before-data.test.js +0 -66
  71. package/test/user.test.js +0 -173
  72. package/test/wire.test.js +0 -158
package/.dockerignore DELETED
@@ -1,9 +0,0 @@
1
- node_modules
2
- npm-debug.log
3
- .git
4
- .gitignore
5
- .github
6
- README.md
7
- .DS_Store
8
- build
9
- .env
@@ -1,31 +0,0 @@
1
- # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
- # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
-
4
- name: Node.js CI
5
-
6
- on:
7
- push:
8
- branches: [ "main" ]
9
- pull_request:
10
- branches: [ "main" ]
11
-
12
- jobs:
13
- build:
14
-
15
- runs-on: ubuntu-latest
16
-
17
- strategy:
18
- matrix:
19
- node-version: [18.x, 20.x, 22.x]
20
- # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21
-
22
- steps:
23
- - uses: actions/checkout@v4
24
- - name: Use Node.js ${{ matrix.node-version }}
25
- uses: actions/setup-node@v4
26
- with:
27
- node-version: ${{ matrix.node-version }}
28
- cache: 'npm'
29
- - run: npm ci
30
- - run: npm run build --if-present
31
- - run: npm test
@@ -1,33 +0,0 @@
1
- # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
- # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
-
4
- name: Node.js Package
5
-
6
- on:
7
- release:
8
- types: [created]
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- - uses: actions/setup-node@v4
16
- with:
17
- node-version: 20
18
- - run: npm ci
19
- - run: npm test
20
-
21
- publish-npm:
22
- needs: build
23
- runs-on: ubuntu-latest
24
- steps:
25
- - uses: actions/checkout@v4
26
- - uses: actions/setup-node@v4
27
- with:
28
- node-version: 20
29
- registry-url: https://registry.npmjs.org/
30
- - run: npm ci
31
- - run: npm publish
32
- env:
33
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/Dockerfile DELETED
@@ -1,20 +0,0 @@
1
- # Use official Node.js runtime as base image
2
- FROM node:20-alpine
3
-
4
- # Set working directory in container
5
- WORKDIR /app
6
-
7
- # Copy package.json and package-lock.json (if available)
8
- COPY package*.json ./
9
-
10
- # Install dependencies
11
- RUN npm install
12
-
13
- # Copy the rest of the application code
14
- COPY . .
15
-
16
- # Expose the HTTP port and WebSocket port
17
- EXPOSE 3000 8765
18
-
19
- # Run the server
20
- CMD ["node", "src/index.js"]
@@ -1,33 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Holster - test examples</title>
6
- </head>
7
- <body>
8
- <h2>Holster - test examples</h2>
9
- <p>
10
- This file loads <code>index.js</code> which contains examples that use the
11
- Holster API. See the console log for test output.
12
- </p>
13
- <p>
14
- The tests make two updates to the same data, but they occur in random
15
- order. The "species" field is then logged and should match the update that
16
- happened last.
17
- </p>
18
- <p>
19
- The <code>index.js</code> file is loaded as a module, which allows it to
20
- import the development build of Holster. You can switch to the production
21
- build by changing the import path at the top of the file.
22
- </p>
23
- <p>
24
- See <a href="/examples/user.html">user.html</a> for examples that test the
25
- Holster.user API.
26
- </p>
27
- <p>
28
- See <a href="/examples/wire.html">wire.html</a> for examples that test the
29
- Holster.wire API.
30
- </p>
31
- <script type="module" src="/examples/index.js"></script>
32
- </body>
33
- </html>
package/examples/index.js DELETED
@@ -1,49 +0,0 @@
1
- // Switch src to build for the production version.
2
- import Holster from "../src/holster.js"
3
-
4
- // Set readFromDisk to true to force reading from disk.
5
- // (Restart the server so that graph is not in memory.)
6
- const readFromDisk = false
7
-
8
- const holster = Holster({indexedDB: !readFromDisk})
9
- globalThis.holster = holster
10
-
11
- if (readFromDisk) {
12
- // Wait for the websocket to connect.
13
- setTimeout(() => {
14
- holster.get("mark").next("boss", {".": "species"}, data => {
15
- console.log("read from disk:", data)
16
- })
17
- }, 1000)
18
- } else {
19
- // Create two updates that could happen in either order.
20
- setTimeout(() => {
21
- const update1 = {
22
- name: "Mark Nadal",
23
- boss: {
24
- name: "Fluffy",
25
- species: "a kitty",
26
- },
27
- }
28
- holster.get("mark").put(update1, err => console.log("update1:", err))
29
- }, 1000 * Math.random())
30
-
31
- setTimeout(() => {
32
- const update2 = {
33
- name: "Mark",
34
- boss: {
35
- species: "felis silvestris",
36
- color: "ginger",
37
- },
38
- }
39
- holster.get("mark").put(update2, err => console.log("update2:", err))
40
- }, 1000 * Math.random())
41
-
42
- // The API sets the state on properties to the current time, so the result
43
- // here depends on which update was called first.
44
- setTimeout(() => {
45
- holster.get("mark").next("boss", {".": "species"}, data => {
46
- console.log("get:", data)
47
- })
48
- }, 2000)
49
- }
@@ -1,31 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Holster.user - test examples</title>
6
- </head>
7
- <body>
8
- <h2>Holster.user - test examples</h2>
9
- <p>
10
- This file loads <code>user.js</code> which contains examples that test the
11
- Holster.user API. See the console log for test output.
12
- </p>
13
- <p>
14
- The tests create two users, Alice and Bob. They both write data to their
15
- own user space. Bob writes secret data that can only be read by Alice,
16
- then Alice logs in again to read the message.
17
- </p>
18
- <p>
19
- These tests also show how to use the SEA API (Security, Encryption, and
20
- Authorization), which can be loaded on it's own, but it made available via
21
- holster.SEA. The holster.user API uses the SEA API internally for creating
22
- and authenticating users.
23
- </p>
24
- <p>
25
- The <code>user.js</code> file is loaded as a module, which allows it to
26
- import the development build of Holster. You can switch to the production
27
- build by changing the import path at the top of the file.
28
- </p>
29
- <script type="module" src="./user.js"></script>
30
- </body>
31
- </html>
package/examples/user.js DELETED
@@ -1,93 +0,0 @@
1
- // Switch src to build for the production version.
2
- import Holster from "../src/holster.js"
3
-
4
- const holster = Holster({indexedDB: true})
5
- globalThis.holster = holster
6
-
7
- const user = holster.user()
8
-
9
- let alice, bob
10
- // Wait for the websocket to connect.
11
- setTimeout(() => {
12
- user.create("alice", "password", err => {
13
- if (err && err !== "Username already exists") {
14
- console.log(err)
15
- return
16
- }
17
-
18
- user.auth("alice", "password", err => {
19
- if (err) {
20
- console.log(err)
21
- return
22
- }
23
-
24
- // Store Alice's encryption key so Bob can use it to create shared data.
25
- alice = {epub: user.is.epub}
26
- console.log("Alice logged in.", user.is)
27
-
28
- const data = "Success! Only Alice can put data on their account"
29
- user.get("protected").put(data, err => {
30
- if (err) {
31
- console.log(err)
32
- return
33
- }
34
-
35
- user.get("protected", console.log)
36
- })
37
- })
38
- })
39
- }, 1000)
40
-
41
- setTimeout(() => {
42
- user.create("bob", "stronger password", err => {
43
- if (err && err !== "Username already exists") {
44
- console.log(err)
45
- return
46
- }
47
-
48
- user.auth("bob", "stronger password", async err => {
49
- if (err) {
50
- console.log(err)
51
- return
52
- }
53
-
54
- // Store Bob's public keys so Alice can use them to read shared data.
55
- bob = {pub: user.is.pub, epub: user.is.epub}
56
- console.log("Bob logged in.", user.is)
57
-
58
- // For this test let's create shared private data for Alice.
59
- const secret = await holster.SEA.secret(alice, user.is)
60
- const shared = await holster.SEA.encrypt("Hello Alice!", secret)
61
- user.get("shared").put(shared, err => {
62
- if (err) {
63
- console.log(err)
64
- return
65
- }
66
-
67
- console.log("Success! Bob wrote secret data to his account.")
68
- })
69
- })
70
- })
71
- }, 2000)
72
-
73
- setTimeout(() => {
74
- if (!bob) {
75
- console.log("Something went wrong, bob didn't log in.")
76
- return
77
- }
78
-
79
- // Use Bob's public key to fetch shared data under his account.
80
- user.get([bob.pub, "shared"], enc => {
81
- user.auth("alice", "password", async err => {
82
- if (err) {
83
- console.log(err)
84
- return
85
- }
86
-
87
- console.log("Alice logged in again.", user.is)
88
- const secret = await holster.SEA.secret(bob, user.is)
89
- const shared = await holster.SEA.decrypt(enc, secret)
90
- console.log("Alice's shared data from Bob:", shared)
91
- })
92
- })
93
- }, 3000)
@@ -1,27 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Holster.wire - test examples</title>
6
- </head>
7
- <body>
8
- <h2>Holster.wire - test examples</h2>
9
- <p>
10
- This file loads <code>wire.js</code> which contains examples that use the
11
- Holster.wire API. These examples show how conflict resolution works, see
12
- the console log for test output.
13
- </p>
14
- <p>
15
- These tests also make two updates in random order, but the wire API can
16
- set state vectors on updates to control which value should be applied.
17
- This means when the "species" field is logged it will always be the same
18
- no matter which order the tests run in.
19
- </p>
20
- <p>
21
- The <code>wire.js</code> file is loaded as a module, which allows it to
22
- import the development build of Holster. You can switch to the production
23
- build by changing the import path at the top of the file.
24
- </p>
25
- <script type="module" src="./wire.js"></script>
26
- </body>
27
- </html>
package/examples/wire.js DELETED
@@ -1,55 +0,0 @@
1
- // Switch src to build for the production version.
2
- import Holster from "../src/holster.js"
3
-
4
- // Set readFromDisk to true to force reading from disk.
5
- // (Restart the server so that graph is not in memory.)
6
- const readFromDisk = false
7
-
8
- const holster = Holster({indexedDB: !readFromDisk})
9
- globalThis.holster = holster
10
-
11
- if (readFromDisk) {
12
- // Wait for the websocket to connect.
13
- setTimeout(() => {
14
- holster.wire.get({"#": "FDSA", ".": "species"}, msg => {
15
- console.log("read from disk:", msg)
16
- })
17
- }, 1000)
18
- } else {
19
- // Create two updates that could happen in either order.
20
- setTimeout(() => {
21
- const update1 = {
22
- ASDF: {
23
- _: {"#": "ASDF", ">": {name: 2, boss: 2}},
24
- name: "Mark Nadal",
25
- boss: {"#": "FDSA"},
26
- },
27
- FDSA: {
28
- _: {"#": "FDSA", ">": {name: 2, species: 2, slave: 2}},
29
- name: "Fluffy",
30
- species: "a kitty",
31
- slave: {"#": "ASDF"},
32
- },
33
- }
34
- holster.wire.put(update1, msg => console.log("update1:", msg))
35
- }, 1000 * Math.random())
36
-
37
- setTimeout(() => {
38
- const update2 = {
39
- ASDF: {_: {"#": "ASDF", ">": {name: 1}}, name: "Mark"},
40
- FDSA: {
41
- _: {"#": "FDSA", ">": {species: 2, color: 3}},
42
- species: "felis silvestris",
43
- color: "ginger",
44
- },
45
- }
46
- holster.wire.put(update2, msg => console.log("update2:", msg))
47
- }, 1000 * Math.random())
48
-
49
- // They should always produce the same result no matter which order.
50
- setTimeout(() => {
51
- holster.wire.get({"#": "FDSA", ".": "species"}, msg => {
52
- console.log("get:", msg)
53
- })
54
- }, 2000)
55
- }
@@ -1,94 +0,0 @@
1
- import fs from "fs"
2
- import {Server} from "mock-socket"
3
- import {describe, test} from "node:test"
4
- import assert from "node:assert/strict"
5
- import Holster from "../src/holster.js"
6
-
7
- describe("holster.get", () => {
8
- const wss = new Server("ws://localhost:1234")
9
- const holster = Holster({file: "test/holster.get", wss: wss, maxAge: 100})
10
-
11
- test("empty key callback null", (t, done) => {
12
- holster.get("", data => {
13
- assert.equal(data, null)
14
- done()
15
- })
16
- })
17
-
18
- test("null key callback null", (t, done) => {
19
- holster.get(null, data => {
20
- assert.equal(data, null)
21
- done()
22
- })
23
- })
24
-
25
- test("underscore as key callback null", (t, done) => {
26
- holster.get("_", data => {
27
- assert.equal(data, null)
28
- done()
29
- })
30
- })
31
-
32
- test("get unknown key callback null", (t, done) => {
33
- holster.get("unknown", data => {
34
- assert.equal(data, null)
35
- done()
36
- })
37
- })
38
-
39
- test("get unknown keys in for loop callbacks null", (t, done) => {
40
- let count = 0
41
- for (let i = 0; i < 5; i++) {
42
- holster.get("unknown" + i, data => {
43
- assert.equal(data, null)
44
- count++
45
- })
46
- }
47
- setTimeout(() => {
48
- if (count === 5) done()
49
- }, 200)
50
- })
51
-
52
- test("nested unknown keys both callbacks null", (t, done) => {
53
- holster.get("unknown", data => {
54
- assert.equal(data, null)
55
-
56
- holster.get("unknown", data => {
57
- assert.equal(data, null)
58
- done()
59
- })
60
- })
61
- })
62
-
63
- test("get already called callback null", (t, done) => {
64
- holster.get("chained").get("unknown", data => {
65
- assert.equal(data, null)
66
- done()
67
- })
68
- })
69
-
70
- test("next chained unknown keys callback null", (t, done) => {
71
- holster.get("chained").next("unknown", data => {
72
- assert.equal(data, null)
73
- done()
74
- })
75
- })
76
-
77
- test("nested next chained unknown keys callback null", (t, done) => {
78
- holster.get("chained").next("unknown", data => {
79
- assert.equal(data, null)
80
-
81
- holster.get("chained").next("unknown", data => {
82
- assert.equal(data, null)
83
- done()
84
- })
85
- })
86
- })
87
-
88
- test("cleanup", (t, done) => {
89
- fs.rm("test/holster.get", {recursive: true, force: true}, err => {
90
- assert.equal(err, null)
91
- done()
92
- })
93
- })
94
- })
@@ -1,196 +0,0 @@
1
- import fs from "fs"
2
- import {Server} from "mock-socket"
3
- import {describe, test} from "node:test"
4
- import assert from "node:assert/strict"
5
- import Holster from "../src/holster.js"
6
-
7
- describe("holster.lex", () => {
8
- const wss = new Server("ws://localhost:1234")
9
- const holster = Holster({file: "test/holster.lex", wss: wss, maxAge: 100})
10
-
11
- test("object on root in graph format", (t, done) => {
12
- const plain = {
13
- key: "plain value",
14
- true: true,
15
- false: false,
16
- number: 42,
17
- }
18
- holster.get("plain").put(plain, err => {
19
- assert.equal(err, null)
20
-
21
- // Prefix tests. (Need to be sequential to match return values.)
22
- holster.get("plain", {".": {"*": "k"}}, data => {
23
- assert.deepEqual(data, {key: "plain value"})
24
-
25
- holster.get("plain", {".": {"*": "t"}}, data => {
26
- assert.deepEqual(data, {true: true})
27
-
28
- holster.get("plain", {".": {"*": "f"}}, data => {
29
- assert.deepEqual(data, {false: false})
30
-
31
- holster.get("plain", {".": {"*": "n"}}, data => {
32
- assert.deepEqual(data, {number: 42})
33
- })
34
-
35
- // Both less than and greater than.
36
- holster.get("plain", {".": {"<": "n", ">": "falsy"}}, data => {
37
- assert.deepEqual(data, {key: "plain value"})
38
-
39
- // Only less than.
40
- holster.get("plain", {".": {"<": "k"}}, data => {
41
- assert.deepEqual(data, {false: false})
42
-
43
- // Only greater than.
44
- holster.get("plain", {".": {">": "numbers"}}, data => {
45
- assert.deepEqual(data, {true: true})
46
- done()
47
- })
48
- })
49
- })
50
- })
51
- })
52
- })
53
- })
54
- })
55
-
56
- test("put and get nested object", (t, done) => {
57
- const nested = {
58
- key: "nested value",
59
- other: "other value",
60
- child: {
61
- has: "child value",
62
- other: "other child value",
63
- },
64
- }
65
- holster.get("nested").put(nested, err => {
66
- assert.equal(err, null)
67
-
68
- // Getting a nested object requires waiting for radisk to write to disk,
69
- // as it will batch the writes. (Default wait is 1 millisecond.)
70
- setTimeout(() => {
71
- holster.get("nested", {".": {"*": "k"}}, data => {
72
- assert.deepEqual(data, {key: "nested value"})
73
-
74
- holster.get("nested").next("child", {".": {"*": "h"}}, data => {
75
- assert.deepEqual(data, {has: "child value"})
76
-
77
- holster.get("nested").next("child", {".": {"<": "has"}}, data => {
78
- // Less than means less than or equal to in lex.
79
- assert.deepEqual(data, {has: "child value"})
80
- done()
81
- })
82
- })
83
- })
84
- }, 2)
85
- })
86
- })
87
-
88
- test("chained get before put nested object", (t, done) => {
89
- const nested = {
90
- key: "hello nested value",
91
- other: "other value",
92
- child: {
93
- has: "hello child value",
94
- other: "other child value",
95
- },
96
- }
97
- holster
98
- .get("hello")
99
- .next("nested")
100
- .put(nested, err => {
101
- assert.equal(err, null)
102
-
103
- setTimeout(() => {
104
- holster.get("hello").next("nested", {".": {"*": "k"}}, data => {
105
- assert.deepEqual(data, {key: "hello nested value"})
106
-
107
- holster
108
- .get("hello")
109
- .next("nested")
110
- .next("child", {".": {"*": "h"}}, data => {
111
- assert.deepEqual(data, {has: "hello child value"})
112
-
113
- holster
114
- .get("hello")
115
- .next("nested")
116
- .next("child", {".": {">": "other"}}, data => {
117
- // Greater than means greater than or equal to in lex.
118
- assert.deepEqual(data, {other: "other child value"})
119
- done()
120
- })
121
- })
122
- })
123
- }, 2)
124
- })
125
- })
126
-
127
- test("put and get two nested objects", (t, done) => {
128
- const two = {
129
- key: "two nested values",
130
- child1: {
131
- has: "child value 1",
132
- },
133
- child2: {
134
- has: "child value 2",
135
- },
136
- }
137
- holster.get("two").put(two, err => {
138
- assert.equal(err, null)
139
-
140
- setTimeout(() => {
141
- holster.get("two", {".": {"*": "child"}}, data => {
142
- assert.deepEqual(data, {
143
- child1: two.child1,
144
- child2: two.child2,
145
- })
146
-
147
- holster
148
- .get("two")
149
- .next("child1", {".": {">": "g", "<": "i"}}, data => {
150
- assert.deepEqual(data, {has: "child value 1"})
151
- done()
152
- })
153
- })
154
- }, 2)
155
- })
156
- })
157
-
158
- test("on and put nested object", (t, done) => {
159
- const nested = {
160
- key: "nested key",
161
- child: {
162
- has: "child key",
163
- },
164
- }
165
- // The node needs to exist before it can be listend to for updates.
166
- holster.get("on-nested").put(nested, err => {
167
- assert.equal(err, null)
168
-
169
- const update = {
170
- child: {
171
- has: "child update",
172
- },
173
- }
174
- holster
175
- .get("on-nested")
176
- .next("child")
177
- .on({".": {">": "h"}}, data => {
178
- assert.deepEqual(data, update.child)
179
- done()
180
- })
181
-
182
- setTimeout(() => {
183
- holster.get("on-nested").put(update, err => {
184
- assert.equal(err, null)
185
- })
186
- }, 10)
187
- })
188
- })
189
-
190
- test("cleanup", (t, done) => {
191
- fs.rm("test/holster.lex", {recursive: true, force: true}, err => {
192
- assert.equal(err, null)
193
- done()
194
- })
195
- })
196
- })