@mblaney/holster 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblaney/holster",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Holster is a service that allows synchronising data between devices using Node, Deno, Bun or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/ham.js CHANGED
@@ -11,11 +11,10 @@ const Ham = (state, currentState, value, currentValue, signed = false) => {
11
11
 
12
12
  if (state > currentState) return {incoming: true}
13
13
 
14
- // state is equal to currentState
15
- // If using signed timestamps then reject conflicting values because the
16
- // owner can only create one value per timestamp.
14
+ // state is equal to currentState. Old data is expected to propagate through
15
+ // the network, but conflicting values with same signed timestamp are ignored
16
+ // since the owner can only create one value per timestamp.
17
17
  if (signed && value !== currentValue) {
18
- console.log(`Signed timestamp ${state}: reject conflicting value`)
19
18
  return {historical: true}
20
19
  }
21
20
 
@@ -96,15 +95,36 @@ Ham.mix = async (change, graph, secure, listen) => {
96
95
  const signedTimestamps = new Set()
97
96
  const signedProperties = new Set()
98
97
 
99
- // Verify timestamp signatures (numeric keys in node._["s"])
98
+ // Skip verification of timestamps that are older than what's in the graph
99
+ // to prevent old signatures from distributed messages causing conflicts
100
+ const incomingTimestamps = Object.entries(node._[">"] || {})
101
+
100
102
  for (const [key, sig] of Object.entries(node._["s"] || {})) {
101
103
  const asNumber = Number(key)
102
104
  if (!isNaN(asNumber) && asNumber > 0) {
105
+ // Check if this timestamp is for properties that already have newer
106
+ // versions in graph.
107
+ const hasNewerInGraph = incomingTimestamps.some(([prop, ts]) => {
108
+ if (Number(ts) === asNumber) {
109
+ const graphState =
110
+ graph[soul] && graph[soul]._[">"] ? graph[soul]._[">"][prop] : 0
111
+ // Skip verification if graph has a newer timestamp for property.
112
+ if (graphState > asNumber) {
113
+ return true
114
+ }
115
+ }
116
+ return false
117
+ })
118
+
119
+ if (hasNewerInGraph) {
120
+ continue
121
+ }
122
+
103
123
  const isValid = await SEA.verifyTimestamp(asNumber, sig, pub)
104
124
  if (isValid) {
105
125
  signedTimestamps.add(asNumber)
106
126
  // Add all properties with this timestamp to signed set
107
- for (const [prop, ts] of Object.entries(node._[">"] || {})) {
127
+ for (const [prop, ts] of incomingTimestamps) {
108
128
  if (Number(ts) === asNumber) {
109
129
  signedProperties.add(prop)
110
130
  }
@@ -113,9 +133,27 @@ Ham.mix = async (change, graph, secure, listen) => {
113
133
  }
114
134
  }
115
135
 
116
- // Verify per-property signatures (backward compatibility)
117
- const perPropertySignedProps = await SEA.verifyProperties(node, pub)
118
- perPropertySignedProps.forEach(prop => signedProperties.add(prop))
136
+ // TODO: Remove per-property signature verification after old data has
137
+ // been cleared. Only check per-property signatures if there are
138
+ // properties without timestamp signatures.
139
+ const propertiesWithoutTimestampSigs = Object.keys(node).filter(k => {
140
+ if (k === "_" || k === utils.userPublicKey) return false
141
+ return !incomingTimestamps.some(
142
+ ([prop, ts]) => prop === k && signedTimestamps.has(Number(ts)),
143
+ )
144
+ })
145
+
146
+ if (propertiesWithoutTimestampSigs.length > 0) {
147
+ const nodeToVerify = {_: node._}
148
+ for (const prop of propertiesWithoutTimestampSigs) {
149
+ nodeToVerify[prop] = node[prop]
150
+ }
151
+ const perPropertySignedProps = await SEA.verifyProperties(
152
+ nodeToVerify,
153
+ pub,
154
+ )
155
+ perPropertySignedProps.forEach(prop => signedProperties.add(prop))
156
+ }
119
157
 
120
158
  // If no properties verified, skip this update
121
159
  if (signedProperties.size === 0) {
package/src/holster.js CHANGED
@@ -9,7 +9,7 @@ const Holster = opt => {
9
9
  else if (!utils.obj.is(opt)) opt = {}
10
10
 
11
11
  const wire = Wire(opt)
12
- const user = User(null, wire)
12
+ const user = User(opt, wire)
13
13
  // Map callbacks since the user's callback is not passed to wire.on.
14
14
  const map = new Map()
15
15
  // Allow concurrent calls to the api by storing each context.
package/src/user.js CHANGED
@@ -5,6 +5,7 @@ import {userPublicKey, userSignature} from "./utils.js"
5
5
 
6
6
  const User = (opt, wire) => {
7
7
  if (!wire) wire = Wire(opt)
8
+ const wait = opt?.wait ?? 5000
8
9
  let pubs = []
9
10
  let creating = false
10
11
  let authing = false
@@ -75,7 +76,7 @@ const User = (opt, wire) => {
75
76
 
76
77
  done(null)
77
78
  },
78
- {wait: 5000},
79
+ {wait: wait},
79
80
  )
80
81
  }
81
82
 
@@ -102,7 +103,7 @@ const User = (opt, wire) => {
102
103
  pubs = Object.keys(data)
103
104
  next()
104
105
  },
105
- {wait: 5000},
106
+ {wait: wait},
106
107
  )
107
108
  }
108
109
 
@@ -182,7 +183,7 @@ const User = (opt, wire) => {
182
183
  })
183
184
  })
184
185
  },
185
- {wait: 5000},
186
+ {wait: wait},
186
187
  )
187
188
  },
188
189
  auth: (username, password, cb) => {
package/src/utils.js CHANGED
@@ -89,21 +89,8 @@ export const graph = (soul, data, sig, pub, timestamp) => {
89
89
  g[soul]._[">"][key] = timestamp || Date.now()
90
90
  }
91
91
  }
92
- // Store signatures in node._["s"]
93
- // sig can be:
94
- // - object: per-property signatures (legacy format)
95
- // - string: timestamp signature (new format)
96
92
  if (sig && pub && timestamp) {
97
- g[soul]._["s"] = {}
98
- if (typeof sig === "string") {
99
- // Timestamp signature: store with timestamp as key
100
- g[soul]._["s"][timestamp] = sig
101
- } else if (typeof sig === "object") {
102
- // Per-property signatures: store with property names as keys
103
- for (const [key, signature] of Object.entries(sig)) {
104
- g[soul]._["s"][key] = signature
105
- }
106
- }
93
+ g[soul]._["s"] = {[timestamp]: sig}
107
94
  g[soul][userPublicKey] = pub
108
95
  g[soul]._[">"][userPublicKey] = timestamp
109
96
  }
package/src/wire.js CHANGED
@@ -589,11 +589,11 @@ const Wire = opt => {
589
589
  return
590
590
  }
591
591
 
592
- const processMessage = () => {
592
+ const processMessage = async () => {
593
593
  dup.track(msg["#"])
594
594
 
595
595
  if (msg.get) get(msg, send)
596
- if (msg.put) put(msg, send)
596
+ if (msg.put) await put(msg, send)
597
597
  send(data, isBinary)
598
598
 
599
599
  const id = msg["@"]
@@ -807,7 +807,7 @@ const Wire = opt => {
807
807
  if (Object.keys(filteredPut).length > 0) {
808
808
  // Create filtered message for Ham.mix with original message ID
809
809
  const filteredMsg = {put: filteredPut, "#": msg["#"]}
810
- put(filteredMsg, send)
810
+ await put(filteredMsg, send)
811
811
  }
812
812
  }
813
813
 
@@ -10,6 +10,7 @@ describe("holster.user.get", () => {
10
10
  file: "test/holster.user.get",
11
11
  wss: wss,
12
12
  maxAge: 100,
13
+ wait: 500,
13
14
  })
14
15
  const user = holster.user()
15
16
 
@@ -10,6 +10,7 @@ describe("holster.user.lex", () => {
10
10
  file: "test/holster.user.lex",
11
11
  wss: wss,
12
12
  maxAge: 100,
13
+ wait: 500,
13
14
  })
14
15
  const user = holster.user()
15
16
 
@@ -10,6 +10,7 @@ describe("holster.user.off", () => {
10
10
  file: "test/holster.user.off",
11
11
  wss: wss,
12
12
  maxAge: 100,
13
+ wait: 500,
13
14
  })
14
15
  const user = holster.user()
15
16
 
@@ -6,7 +6,12 @@ import Holster from "../src/holster.js"
6
6
 
7
7
  describe("holster.user.on", () => {
8
8
  const wss = new Server("ws://localhost:1234")
9
- const holster = Holster({file: "test/holster.user.on", wss: wss, maxAge: 100})
9
+ const holster = Holster({
10
+ file: "test/holster.user.on",
11
+ wss: wss,
12
+ maxAge: 100,
13
+ wait: 500,
14
+ })
10
15
  const user = holster.user()
11
16
 
12
17
  test("user create", (t, done) => {
@@ -10,6 +10,7 @@ describe("holster.user.put", () => {
10
10
  file: "test/holster.user.put",
11
11
  wss: wss,
12
12
  maxAge: 100,
13
+ wait: 500,
13
14
  })
14
15
  const user = holster.user()
15
16
  let alice = ""
@@ -6,7 +6,11 @@ import Holster from "../../src/holster.js"
6
6
 
7
7
  describe("system - user chained get with on", () => {
8
8
  const wss = new Server("ws://localhost:9012")
9
- const holster = Holster({file: "test/system/user-chained-on", wss: wss})
9
+ const holster = Holster({
10
+ file: "test/system/user-chained-on",
11
+ wss: wss,
12
+ wait: 500,
13
+ })
10
14
  const user = holster.user()
11
15
 
12
16
  test("user create and auth", (t, done) => {
@@ -9,6 +9,7 @@ describe("system - user concurrent listeners", () => {
9
9
  const holster = Holster({
10
10
  file: "test/system/user-concurrent-listeners",
11
11
  wss: wss,
12
+ wait: 500,
12
13
  })
13
14
  const user = holster.user()
14
15
 
@@ -9,6 +9,7 @@ describe("system - user listener before data", () => {
9
9
  const holster = Holster({
10
10
  file: "test/system/user-listener-before-data",
11
11
  wss: wss,
12
+ wait: 500,
12
13
  })
13
14
  const user = holster.user()
14
15
 
package/test/user.test.js CHANGED
@@ -6,7 +6,7 @@ import User from "../src/user.js"
6
6
 
7
7
  describe("user", () => {
8
8
  const wss = new Server("ws://localhost:1234")
9
- const user = User({file: "test/user", wss: wss, maxAge: 100})
9
+ const user = User({file: "test/user", wss: wss, maxAge: 100, wait: 500})
10
10
 
11
11
  test("create", (t, done) => {
12
12
  user.create("alice", "password", err => {