@mblaney/holster 1.0.7 → 1.0.8
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/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/array.js +21 -10
- package/src/buffer.js +388 -55
- package/src/dup.js +2 -0
- package/src/get.js +11 -0
- package/src/ham.js +31 -5
- package/src/store.js +21 -7
- package/src/utils.js +23 -14
- package/src/wire.js +7 -1
package/package.json
CHANGED
package/src/array.js
CHANGED
|
@@ -5,29 +5,40 @@ if (typeof btoa === "undefined") {
|
|
|
5
5
|
|
|
6
6
|
// This is Array extended to have .toString(["utf8"|"hex"|"base64"])
|
|
7
7
|
function SeaArray() {}
|
|
8
|
-
|
|
9
8
|
Object.assign(SeaArray, {from: Array.from})
|
|
10
9
|
SeaArray.prototype = Object.create(Array.prototype)
|
|
10
|
+
|
|
11
11
|
SeaArray.prototype.toString = function (enc, start, end) {
|
|
12
12
|
if (!enc) enc = "utf8"
|
|
13
13
|
if (!start) start = 0
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
end = end ? Math.min(Math.max(end, start), this.length) : this.length
|
|
16
|
+
|
|
16
17
|
if (enc === "hex") {
|
|
17
|
-
const buf = new Uint8Array(this)
|
|
18
|
-
return
|
|
19
|
-
.map(i => buf[i + start].toString(16).padStart(2, "0"))
|
|
20
|
-
.join("")
|
|
18
|
+
const buf = new Uint8Array(this.slice(start, end))
|
|
19
|
+
return Array.from(buf, byte => byte.toString(16).padStart(2, "0")).join("")
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
if (enc === "utf8") {
|
|
24
|
-
return Array.from({length:
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
return Array.from({length: end - start}, (_, i) => {
|
|
24
|
+
const charCode = this[i + start]
|
|
25
|
+
// Use unicode replacement character for invalid character codes.
|
|
26
|
+
if (charCode < 0 || charCode > 0x10ffff) {
|
|
27
|
+
return String.fromCharCode(0xfffd)
|
|
28
|
+
}
|
|
29
|
+
return String.fromCharCode(charCode)
|
|
30
|
+
}).join("")
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
if (enc === "base64") {
|
|
30
|
-
|
|
34
|
+
const utf8String = Array.from({length: end - start}, (_, i) => {
|
|
35
|
+
const charCode = this[i + start]
|
|
36
|
+
if (charCode < 0 || charCode > 255) {
|
|
37
|
+
return String.fromCharCode(0xfffd)
|
|
38
|
+
}
|
|
39
|
+
return String.fromCharCode(charCode)
|
|
40
|
+
}).join("")
|
|
41
|
+
return btoa(utf8String)
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
package/src/buffer.js
CHANGED
|
@@ -1,83 +1,416 @@
|
|
|
1
1
|
import SeaArray from "./array.js"
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
// Security constants
|
|
4
|
+
const SECURITY_LIMITS = {
|
|
5
|
+
MAX_BUFFER_SIZE: 10 * 1024 * 1024, // 10MB limit
|
|
6
|
+
MAX_STRING_LENGTH: 1024 * 1024, // 1MB string limit
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Secure Buffer implementation for SEA.js
|
|
10
|
+
// Compatible with NodeJS safe-buffer but with enhanced security
|
|
8
11
|
function SafeBuffer(...props) {
|
|
9
12
|
console.warn("new SafeBuffer() is deprecated, please use SafeBuffer.from()")
|
|
10
13
|
return SafeBuffer.from(...props)
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
SafeBuffer.prototype = Object.create(Array.prototype)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
// Input validation helpers
|
|
19
|
+
const validateStringInput = (input, encoding) => {
|
|
20
|
+
if (typeof input !== "string") {
|
|
21
|
+
throw new TypeError("Input must be a string for encoding: " + encoding)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (input.length > SECURITY_LIMITS.MAX_STRING_LENGTH) {
|
|
25
|
+
throw new RangeError(
|
|
26
|
+
`String too large: ${input.length} > ${SECURITY_LIMITS.MAX_STRING_LENGTH}`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const validateBufferSize = (size, operation = "buffer operation") => {
|
|
32
|
+
if (size > SECURITY_LIMITS.MAX_BUFFER_SIZE) {
|
|
33
|
+
throw new RangeError(
|
|
34
|
+
`Buffer size too large for ${operation}: ${size} > ${SECURITY_LIMITS.MAX_BUFFER_SIZE}`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Secure hex parsing
|
|
40
|
+
const parseHexString = hexString => {
|
|
41
|
+
validateStringInput(hexString, "hex")
|
|
42
|
+
|
|
43
|
+
// Remove any whitespace and convert to lowercase
|
|
44
|
+
const cleanHex = hexString.replace(/\s/g, "").toLowerCase()
|
|
45
|
+
|
|
46
|
+
// Validate hex format: only 0-9, a-f characters
|
|
47
|
+
if (!/^[0-9a-f]*$/.test(cleanHex)) {
|
|
48
|
+
throw new TypeError("Invalid hex string: contains non-hex characters")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Must be even length
|
|
52
|
+
if (cleanHex.length % 2 !== 0) {
|
|
53
|
+
throw new TypeError("Invalid hex string: must have even length")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (cleanHex.length === 0) {
|
|
57
|
+
return new Uint8Array(0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
validateBufferSize(cleanHex.length / 2, "hex parsing")
|
|
61
|
+
|
|
62
|
+
const bytes = new Uint8Array(cleanHex.length / 2)
|
|
63
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
64
|
+
const hexByte = cleanHex.substr(i, 2)
|
|
65
|
+
bytes[i / 2] = parseInt(hexByte, 16)
|
|
66
|
+
}
|
|
67
|
+
return bytes
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const encodeUtf8String = str => {
|
|
71
|
+
validateStringInput(str, "utf8")
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Use standard TextEncoder for proper UTF-8 encoding
|
|
75
|
+
const encoder = new TextEncoder()
|
|
76
|
+
const bytes = encoder.encode(str)
|
|
77
|
+
|
|
78
|
+
validateBufferSize(bytes.length, "UTF-8 encoding")
|
|
79
|
+
return bytes
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new TypeError("Failed to encode UTF-8 string: " + err.message)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const encodeBinaryString = str => {
|
|
86
|
+
validateStringInput(str, "binary")
|
|
87
|
+
|
|
88
|
+
validateBufferSize(str.length, "binary encoding")
|
|
89
|
+
|
|
90
|
+
const bytes = new Uint8Array(str.length)
|
|
91
|
+
for (let i = 0; i < str.length; i++) {
|
|
92
|
+
const charCode = str.charCodeAt(i)
|
|
93
|
+
|
|
94
|
+
// Binary strings should only contain bytes (0-255)
|
|
95
|
+
if (charCode > 255) {
|
|
19
96
|
throw new TypeError(
|
|
20
|
-
|
|
97
|
+
`Invalid binary string: character at position ${i} exceeds byte range (${charCode} > 255)`,
|
|
21
98
|
)
|
|
22
99
|
}
|
|
100
|
+
|
|
101
|
+
bytes[i] = charCode
|
|
102
|
+
}
|
|
103
|
+
return bytes
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Secure base64 decoding
|
|
107
|
+
const parseBase64String = base64String => {
|
|
108
|
+
validateStringInput(base64String, "base64")
|
|
109
|
+
|
|
110
|
+
// Remove any whitespace
|
|
111
|
+
const cleanBase64 = base64String.replace(/\s/g, "")
|
|
112
|
+
|
|
113
|
+
// Validate base64 format
|
|
114
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {
|
|
115
|
+
throw new TypeError("Invalid base64 string: contains invalid characters")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const decodedString = atob(cleanBase64)
|
|
120
|
+
validateBufferSize(decodedString.length, "base64 decoding")
|
|
121
|
+
|
|
122
|
+
const bytes = new Uint8Array(decodedString.length)
|
|
123
|
+
for (let i = 0; i < decodedString.length; i++) {
|
|
124
|
+
bytes[i] = decodedString.charCodeAt(i)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return bytes
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new TypeError("Failed to decode base64 string: " + err.message)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Secure array-like input processing
|
|
134
|
+
const processArrayLikeInput = input => {
|
|
135
|
+
let bytes
|
|
136
|
+
|
|
137
|
+
if (input instanceof ArrayBuffer) {
|
|
138
|
+
validateBufferSize(input.byteLength, "ArrayBuffer processing")
|
|
139
|
+
bytes = new Uint8Array(input)
|
|
140
|
+
} else if (ArrayBuffer.isView(input)) {
|
|
141
|
+
validateBufferSize(
|
|
142
|
+
input.byteLength || input.length,
|
|
143
|
+
"TypedArray processing",
|
|
144
|
+
)
|
|
145
|
+
bytes = new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
|
|
146
|
+
} else if (Array.isArray(input)) {
|
|
147
|
+
validateBufferSize(input.length, "Array processing")
|
|
148
|
+
|
|
149
|
+
// Validate all array elements are valid byte values
|
|
150
|
+
for (let i = 0; i < input.length; i++) {
|
|
151
|
+
const val = input[i]
|
|
152
|
+
if (
|
|
153
|
+
typeof val !== "number" ||
|
|
154
|
+
val < 0 ||
|
|
155
|
+
val > 255 ||
|
|
156
|
+
!Number.isInteger(val)
|
|
157
|
+
) {
|
|
158
|
+
throw new TypeError(
|
|
159
|
+
`Invalid byte value at index ${i}: ${val} (must be integer 0-255)`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
bytes = new Uint8Array(input)
|
|
165
|
+
} else if (
|
|
166
|
+
typeof input === "object" &&
|
|
167
|
+
input !== null &&
|
|
168
|
+
typeof input.length === "number"
|
|
169
|
+
) {
|
|
170
|
+
// Handle array-like objects
|
|
171
|
+
validateBufferSize(input.length, "array-like processing")
|
|
172
|
+
|
|
173
|
+
const arr = Array.from(input)
|
|
174
|
+
for (let i = 0; i < arr.length; i++) {
|
|
175
|
+
const val = arr[i]
|
|
176
|
+
if (
|
|
177
|
+
typeof val !== "number" ||
|
|
178
|
+
val < 0 ||
|
|
179
|
+
val > 255 ||
|
|
180
|
+
!Number.isInteger(val)
|
|
181
|
+
) {
|
|
182
|
+
throw new TypeError(
|
|
183
|
+
`Invalid byte value at index ${i}: ${val} (must be integer 0-255)`,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
bytes = new Uint8Array(arr)
|
|
189
|
+
} else {
|
|
190
|
+
throw new TypeError("Unsupported input type: " + typeof input)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return bytes
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Object.assign(SafeBuffer, {
|
|
197
|
+
from() {
|
|
198
|
+
const argCount = arguments.length
|
|
199
|
+
|
|
200
|
+
if (argCount === 0) {
|
|
201
|
+
throw new TypeError("SafeBuffer.from() requires at least one argument")
|
|
202
|
+
}
|
|
203
|
+
|
|
23
204
|
const input = arguments[0]
|
|
24
|
-
|
|
205
|
+
|
|
206
|
+
if (input === null || input === undefined) {
|
|
207
|
+
throw new TypeError("First argument must not be null or undefined")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let bytes
|
|
211
|
+
|
|
25
212
|
if (typeof input === "string") {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
213
|
+
const encoding = (arguments[1] || "utf8").toLowerCase()
|
|
214
|
+
|
|
215
|
+
switch (encoding) {
|
|
216
|
+
case "hex":
|
|
217
|
+
bytes = parseHexString(input)
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
case "utf8":
|
|
221
|
+
case "utf-8":
|
|
222
|
+
bytes = encodeUtf8String(input)
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
case "binary":
|
|
226
|
+
case "latin1":
|
|
227
|
+
bytes = encodeBinaryString(input)
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
case "base64":
|
|
231
|
+
bytes = parseBase64String(input)
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
case "ascii":
|
|
235
|
+
// ASCII is subset of UTF-8, but validate range
|
|
236
|
+
validateStringInput(input, "ascii")
|
|
237
|
+
for (let i = 0; i < input.length; i++) {
|
|
238
|
+
if (input.charCodeAt(i) > 127) {
|
|
239
|
+
throw new TypeError(
|
|
240
|
+
`Invalid ASCII character at position ${i}: ${input.charCodeAt(i)} > 127`,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
bytes = encodeUtf8String(input) // Safe since ASCII is UTF-8 subset
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
default:
|
|
248
|
+
throw new TypeError("Unknown encoding: " + encoding)
|
|
48
249
|
}
|
|
49
|
-
|
|
250
|
+
} else {
|
|
251
|
+
bytes = processArrayLikeInput(input)
|
|
50
252
|
}
|
|
51
|
-
|
|
52
|
-
if (length) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
253
|
+
|
|
254
|
+
if (!bytes || bytes.length === 0) {
|
|
255
|
+
return SeaArray.from(new Uint8Array(0))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
return SeaArray.from(bytes)
|
|
260
|
+
} catch (err) {
|
|
261
|
+
throw new Error("Failed to create SafeBuffer: " + err.message)
|
|
59
262
|
}
|
|
60
263
|
},
|
|
61
|
-
|
|
264
|
+
|
|
62
265
|
alloc(length, fill = 0) {
|
|
63
|
-
|
|
64
|
-
new
|
|
65
|
-
|
|
266
|
+
if (typeof length !== "number" || !Number.isInteger(length) || length < 0) {
|
|
267
|
+
throw new TypeError("Length must be a non-negative integer")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
validateBufferSize(length, "buffer allocation")
|
|
271
|
+
|
|
272
|
+
// Validate fill value
|
|
273
|
+
if (typeof fill === "number") {
|
|
274
|
+
if (fill < 0 || fill > 255 || !Number.isInteger(fill)) {
|
|
275
|
+
throw new TypeError("Fill value must be an integer between 0 and 255")
|
|
276
|
+
}
|
|
277
|
+
} else if (typeof fill === "string") {
|
|
278
|
+
if (fill.length !== 1) {
|
|
279
|
+
throw new TypeError("Fill string must be exactly one character")
|
|
280
|
+
}
|
|
281
|
+
fill = fill.charCodeAt(0)
|
|
282
|
+
if (fill > 255) {
|
|
283
|
+
throw new TypeError("Fill character code must be <= 255")
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
throw new TypeError("Fill must be a number or single character string")
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const buffer = new Uint8Array(length)
|
|
291
|
+
if (fill !== 0) {
|
|
292
|
+
buffer.fill(fill)
|
|
293
|
+
}
|
|
294
|
+
return SeaArray.from(buffer)
|
|
295
|
+
} catch (err) {
|
|
296
|
+
throw new Error("Failed to allocate buffer: " + err.message)
|
|
297
|
+
}
|
|
66
298
|
},
|
|
67
|
-
|
|
299
|
+
|
|
68
300
|
concat(arr) {
|
|
69
|
-
// octet array
|
|
70
301
|
if (!Array.isArray(arr)) {
|
|
71
|
-
throw new TypeError(
|
|
72
|
-
|
|
73
|
-
|
|
302
|
+
throw new TypeError("First argument must be an array")
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (arr.length === 0) {
|
|
306
|
+
return SeaArray.from(new Uint8Array(0))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Validate all array elements and calculate total size
|
|
310
|
+
let totalLength = 0
|
|
311
|
+
const validatedBuffers = []
|
|
312
|
+
|
|
313
|
+
for (let i = 0; i < arr.length; i++) {
|
|
314
|
+
const item = arr[i]
|
|
315
|
+
|
|
316
|
+
if (!item) {
|
|
317
|
+
throw new TypeError(`Array element at index ${i} is null or undefined`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let bytes
|
|
321
|
+
try {
|
|
322
|
+
bytes = processArrayLikeInput(item)
|
|
323
|
+
} catch (err) {
|
|
324
|
+
throw new TypeError(
|
|
325
|
+
`Invalid array element at index ${i}: ${err.message}`,
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
totalLength += bytes.length
|
|
330
|
+
validateBufferSize(totalLength, "buffer concatenation")
|
|
331
|
+
|
|
332
|
+
validatedBuffers.push(bytes)
|
|
74
333
|
}
|
|
75
|
-
|
|
76
|
-
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const result = new Uint8Array(totalLength)
|
|
337
|
+
let offset = 0
|
|
338
|
+
|
|
339
|
+
for (const buffer of validatedBuffers) {
|
|
340
|
+
result.set(buffer, offset)
|
|
341
|
+
offset += buffer.length
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return SeaArray.from(result)
|
|
345
|
+
} catch (err) {
|
|
346
|
+
throw new Error("Failed to concatenate buffers: " + err.message)
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
// Helper method to check if object is SafeBuffer
|
|
351
|
+
isBuffer(obj) {
|
|
352
|
+
return (
|
|
353
|
+
obj instanceof SeaArray ||
|
|
354
|
+
(obj && typeof obj === "object" && obj.constructor === SafeBuffer)
|
|
77
355
|
)
|
|
78
356
|
},
|
|
357
|
+
|
|
358
|
+
// Get byte length of string in specific encoding
|
|
359
|
+
byteLength(string, encoding = "utf8") {
|
|
360
|
+
if (typeof string !== "string") {
|
|
361
|
+
throw new TypeError("First argument must be a string")
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
switch (encoding.toLowerCase()) {
|
|
365
|
+
case "utf8":
|
|
366
|
+
case "utf-8":
|
|
367
|
+
return new TextEncoder().encode(string).length
|
|
368
|
+
|
|
369
|
+
case "ascii":
|
|
370
|
+
case "binary":
|
|
371
|
+
case "latin1":
|
|
372
|
+
return string.length
|
|
373
|
+
|
|
374
|
+
case "base64":
|
|
375
|
+
const clean = string.replace(/\s/g, "")
|
|
376
|
+
return (
|
|
377
|
+
Math.floor((clean.length * 3) / 4) - (clean.match(/=/g) || []).length
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
case "hex":
|
|
381
|
+
return Math.floor(string.replace(/\s/g, "").length / 2)
|
|
382
|
+
|
|
383
|
+
default:
|
|
384
|
+
throw new TypeError("Unknown encoding: " + encoding)
|
|
385
|
+
}
|
|
386
|
+
},
|
|
79
387
|
})
|
|
388
|
+
|
|
80
389
|
SafeBuffer.prototype.from = SafeBuffer.from
|
|
81
|
-
SafeBuffer.prototype.toString =
|
|
390
|
+
SafeBuffer.prototype.toString = function (
|
|
391
|
+
encoding = "utf8",
|
|
392
|
+
start = 0,
|
|
393
|
+
end = this.length,
|
|
394
|
+
) {
|
|
395
|
+
// Input validation
|
|
396
|
+
if (typeof encoding !== "string") {
|
|
397
|
+
throw new TypeError("Encoding must be a string")
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (typeof start !== "number" || !Number.isInteger(start) || start < 0) {
|
|
401
|
+
throw new TypeError("Start must be a non-negative integer")
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (typeof end !== "number" || !Number.isInteger(end) || end < start) {
|
|
405
|
+
throw new TypeError("End must be an integer >= start")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Delegate to SeaArray with validation
|
|
409
|
+
try {
|
|
410
|
+
return SeaArray.prototype.toString.call(this, encoding, start, end)
|
|
411
|
+
} catch (err) {
|
|
412
|
+
throw new Error("Failed to convert buffer to string: " + err.message)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
82
415
|
|
|
83
416
|
export default SafeBuffer
|
package/src/dup.js
CHANGED
package/src/get.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import {match} from "./utils.js"
|
|
2
2
|
|
|
3
3
|
const Get = (lex, graph) => {
|
|
4
|
+
if (!lex || typeof lex !== "object") {
|
|
5
|
+
throw new TypeError("lex must be an object")
|
|
6
|
+
}
|
|
7
|
+
if (!graph || typeof graph !== "object") {
|
|
8
|
+
throw new TypeError("graph must be an object")
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
const soul = lex["#"]
|
|
12
|
+
if (!soul || typeof soul !== "string") {
|
|
13
|
+
throw new TypeError("soul must be a string")
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
if (!graph[soul]) return
|
|
6
17
|
|
|
7
18
|
const node = {_: {"#": soul, ">": {}}}
|
package/src/ham.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as utils from "./utils.js"
|
|
2
2
|
import SEA from "./sea.js"
|
|
3
3
|
|
|
4
|
+
const LISTENER_DELAY = 100
|
|
5
|
+
// Maximum number of souls to keep in memory
|
|
6
|
+
const MAX_GRAPH_SIZE = 10000
|
|
7
|
+
|
|
4
8
|
// state and value are the incoming changes.
|
|
5
9
|
// currentState and currentValue are the current graph data.
|
|
6
10
|
const Ham = (state, currentState, value, currentValue) => {
|
|
@@ -26,6 +30,16 @@ const Ham = (state, currentState, value, currentValue) => {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
Ham.mix = async (change, graph, secure, listen) => {
|
|
33
|
+
if (!change || typeof change !== "object") {
|
|
34
|
+
throw new TypeError("change must be an object")
|
|
35
|
+
}
|
|
36
|
+
if (!graph || typeof graph !== "object") {
|
|
37
|
+
throw new TypeError("graph must be an object")
|
|
38
|
+
}
|
|
39
|
+
if (!listen || typeof listen !== "object") {
|
|
40
|
+
throw new TypeError("listen must be an object")
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
const machine = Date.now()
|
|
30
44
|
const now = {}
|
|
31
45
|
const defer = {}
|
|
@@ -103,9 +117,6 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
103
117
|
if (!now[soul]) {
|
|
104
118
|
now[soul] = {_: {"#": soul, ">": {}}}
|
|
105
119
|
}
|
|
106
|
-
// TODO: graph should not just grow indefintitely in memory.
|
|
107
|
-
// Need to have a max size after which start dropping the oldest state
|
|
108
|
-
// Do something similar to Dup which can handle deletes?
|
|
109
120
|
if (!graph[soul]) {
|
|
110
121
|
graph[soul] = {_: {"#": soul, ">": {}}}
|
|
111
122
|
}
|
|
@@ -121,7 +132,7 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
121
132
|
.filter(l => utils.match(l["."], key))
|
|
122
133
|
.forEach(l => l.cb())
|
|
123
134
|
}
|
|
124
|
-
},
|
|
135
|
+
}, LISTENER_DELAY)
|
|
125
136
|
}
|
|
126
137
|
updated = true
|
|
127
138
|
}
|
|
@@ -139,9 +150,24 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
139
150
|
if (listen[soul]) {
|
|
140
151
|
listen[soul].filter(l => utils.match(l["."])).forEach(l => l.cb())
|
|
141
152
|
}
|
|
142
|
-
},
|
|
153
|
+
}, LISTENER_DELAY)
|
|
143
154
|
}
|
|
144
155
|
}
|
|
156
|
+
|
|
157
|
+
const souls = Object.keys(graph)
|
|
158
|
+
if (souls.length > MAX_GRAPH_SIZE) {
|
|
159
|
+
// Sort by oldest state timestamp and remove oldest entries
|
|
160
|
+
const soulsByAge = souls
|
|
161
|
+
.map(soul => {
|
|
162
|
+
const states = graph[soul]._ && graph[soul]._[">"]
|
|
163
|
+
const maxState = states ? Math.max(...Object.values(states)) : 0
|
|
164
|
+
return {soul, maxState}
|
|
165
|
+
})
|
|
166
|
+
.sort((a, b) => a.maxState - b.maxState)
|
|
167
|
+
const remove = soulsByAge.slice(0, souls.length - maxSize)
|
|
168
|
+
remove.forEach(({soul}) => delete graph[soul])
|
|
169
|
+
}
|
|
170
|
+
|
|
145
171
|
return {now: now, defer: defer, wait: wait}
|
|
146
172
|
}
|
|
147
173
|
|
package/src/store.js
CHANGED
|
@@ -49,6 +49,12 @@ const fileSystem = opt => {
|
|
|
49
49
|
},
|
|
50
50
|
list: cb => {
|
|
51
51
|
fs.readdir(dir, (err, files) => {
|
|
52
|
+
if (err) {
|
|
53
|
+
console.log("fs.readdir error:", err)
|
|
54
|
+
cb()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
files.forEach(cb)
|
|
53
59
|
cb()
|
|
54
60
|
})
|
|
@@ -206,10 +212,14 @@ const Store = opt => {
|
|
|
206
212
|
|
|
207
213
|
return {
|
|
208
214
|
get: (lex, cb) => {
|
|
209
|
-
if (!lex) {
|
|
215
|
+
if (!lex || !utils.obj.is(lex)) {
|
|
210
216
|
cb("lex required")
|
|
211
217
|
return
|
|
212
218
|
}
|
|
219
|
+
if (!lex["#"]) {
|
|
220
|
+
cb("soul required in lex")
|
|
221
|
+
return
|
|
222
|
+
}
|
|
213
223
|
|
|
214
224
|
var soul = lex["#"]
|
|
215
225
|
var key = typeof lex["."] === "string" ? lex["."] : ""
|
|
@@ -241,18 +251,22 @@ const Store = opt => {
|
|
|
241
251
|
return
|
|
242
252
|
}
|
|
243
253
|
|
|
244
|
-
|
|
254
|
+
let count = 0
|
|
255
|
+
let finished = false
|
|
245
256
|
const ack = err => {
|
|
246
257
|
count--
|
|
247
|
-
if (
|
|
258
|
+
if (finished) return
|
|
248
259
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
cb(
|
|
260
|
+
if (err) {
|
|
261
|
+
finished = true
|
|
262
|
+
cb(err)
|
|
252
263
|
return
|
|
253
264
|
}
|
|
254
265
|
|
|
255
|
-
if (count === 0)
|
|
266
|
+
if (count === 0) {
|
|
267
|
+
finished = true
|
|
268
|
+
cb(null)
|
|
269
|
+
}
|
|
256
270
|
}
|
|
257
271
|
|
|
258
272
|
Object.keys(graph).forEach(soul => {
|