@mblaney/holster 2.1.4 → 2.2.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.
- package/build/holster.js +3 -3
- package/build/holster.js.map +4 -4
- package/dist/holster.js +3 -3
- package/dist/holster.js.map +4 -4
- package/package.json +2 -2
- package/src/holster.js +30 -0
- package/src/nested.js +89 -0
- package/src/wire.js +22 -12
- package/ts/src/holster.ts +40 -7
- package/ts/src/nested.ts +101 -0
- package/ts/src/schemas.ts +1 -0
- package/ts/src/wire.ts +21 -11
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mblaney/holster",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node --test",
|
|
8
|
+
"test": "node --test test/*.test.js test/system/*.test.js",
|
|
9
9
|
"test:ts": "node --test --import tsx/esm ts/test/*.test.ts ts/test/system/*.test.ts",
|
|
10
10
|
"build": "esbuild src/holster.js --bundle --format=esm --sourcemap \"--external:node:*\" \"--external:ws\" --minify --outdir=build",
|
|
11
11
|
"build:ts": "esbuild ts/src/holster.ts --bundle --format=esm --sourcemap \"--external:node:*\" \"--external:ws\" --minify --outdir=dist",
|
package/src/holster.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as utils from "./utils.js"
|
|
|
2
2
|
import Wire from "./wire.js"
|
|
3
3
|
import User from "./user.js"
|
|
4
4
|
import SEA from "./sea.js"
|
|
5
|
+
import {attachNested} from "./nested.js"
|
|
5
6
|
|
|
6
7
|
const Holster = opt => {
|
|
7
8
|
if (typeof opt === "string") opt = {peers: [opt]}
|
|
@@ -14,6 +15,12 @@ const Holster = opt => {
|
|
|
14
15
|
const map = new Map()
|
|
15
16
|
// Allow concurrent calls to the api by storing each context.
|
|
16
17
|
const allctx = new Map()
|
|
18
|
+
// Tracks on(..., {nested: true}) subscriptions, keyed by the caller's
|
|
19
|
+
// own callback (the same reference off() will be called with) - see
|
|
20
|
+
// attachNested in nested.js. The actual top-level/child listeners it
|
|
21
|
+
// creates use their own, separate contexts, so off() just needs to
|
|
22
|
+
// call the detach function this stores to tear the whole thing down.
|
|
23
|
+
const nested = new Map()
|
|
17
24
|
// Serializes concurrent creation of a missing rel for the same
|
|
18
25
|
// soul+item, so only the first caller creates it and the rest reuse
|
|
19
26
|
// its result rather than each minting a competing soul. Map<soul,
|
|
@@ -680,6 +687,23 @@ const Holster = opt => {
|
|
|
680
687
|
// Get the context to check if it has user info
|
|
681
688
|
const ctx = allctx.get(ctxid)
|
|
682
689
|
|
|
690
|
+
if (_opt && _opt.nested) {
|
|
691
|
+
// attachNested calls this once for the top level and again for
|
|
692
|
+
// every discovered child, each needing its own independent
|
|
693
|
+
// context - a single shared _ctxid would accumulate .next()
|
|
694
|
+
// calls onto the same path instead of giving sibling paths.
|
|
695
|
+
const factory = () => {
|
|
696
|
+
const _ctxid = utils.text.random()
|
|
697
|
+
allctx.set(_ctxid, {
|
|
698
|
+
chain: [{item: item, soul: soul}],
|
|
699
|
+
user: ctx ? ctx.user : null,
|
|
700
|
+
})
|
|
701
|
+
return api(_ctxid)
|
|
702
|
+
}
|
|
703
|
+
nested.set(cb, attachNested(factory, cb, _get, _opt))
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
|
|
683
707
|
// Flag that this context is set from on and shouldn't be removed.
|
|
684
708
|
allctx.set(ctxid, {
|
|
685
709
|
chain: [{item: item, soul: soul}],
|
|
@@ -837,6 +861,12 @@ const Holster = opt => {
|
|
|
837
861
|
return
|
|
838
862
|
}
|
|
839
863
|
|
|
864
|
+
if (nested.has(cb)) {
|
|
865
|
+
nested.get(cb)()
|
|
866
|
+
nested.delete(cb)
|
|
867
|
+
return
|
|
868
|
+
}
|
|
869
|
+
|
|
840
870
|
// Resolve the current context before removing event listener.
|
|
841
871
|
const {item, soul} = resolve({off: true}, cb)
|
|
842
872
|
if (!soul) return
|
package/src/nested.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Backs on(cb, _get, {nested: true}) - see holster.js. A plain on() only
|
|
2
|
+
// resolves the node it's given; it never re-checks a child whose own
|
|
3
|
+
// value is an object (which always has its own soul, since only scalars
|
|
4
|
+
// live directly on a node) once that child's content changes in place -
|
|
5
|
+
// nothing on the parent's own soul changes when that happens, so a
|
|
6
|
+
// listener on the parent alone never sees it. get()'s own read-resolution
|
|
7
|
+
// (holster.js) already recursively resolves every such child into plain
|
|
8
|
+
// data for a one-time read - this doesn't repeat that; it adds a live
|
|
9
|
+
// subscription for each child that resolution already exposed, so a
|
|
10
|
+
// later change to it reaches the caller too.
|
|
11
|
+
//
|
|
12
|
+
// Pure and holster-internals-free: chain is a factory - () =>
|
|
13
|
+
// user.get([host, "shared"]).next("domains").next(code) - not a
|
|
14
|
+
// pre-built chained object, since a fresh, independent instance is
|
|
15
|
+
// needed per discovered child (reusing one chained object across
|
|
16
|
+
// multiple .next() calls would push them onto the same path instead of
|
|
17
|
+
// giving sibling paths).
|
|
18
|
+
export function attachNested(chain, cb, _get, _opt) {
|
|
19
|
+
// Stripped before being used on any .on() call this function makes
|
|
20
|
+
// itself (top-level or child) - otherwise a caller-provided {nested:
|
|
21
|
+
// true} would re-trigger holster.js's own nested check on re-entry,
|
|
22
|
+
// before the normal rel-resolution logic that actually finds the right
|
|
23
|
+
// soul ever runs.
|
|
24
|
+
const opt = {..._opt}
|
|
25
|
+
delete opt.nested
|
|
26
|
+
|
|
27
|
+
const childListeners = new Map() // key -> {api, handler}
|
|
28
|
+
let latest = null
|
|
29
|
+
|
|
30
|
+
const notify = () => cb(latest === null ? null : {...latest})
|
|
31
|
+
|
|
32
|
+
const topLevelHandler = data => {
|
|
33
|
+
if (data === null || typeof data !== "object") {
|
|
34
|
+
latest = data
|
|
35
|
+
notify()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!latest || typeof latest !== "object") latest = {}
|
|
40
|
+
for (const [key, value] of Object.entries(data)) {
|
|
41
|
+
if (key === "_") continue
|
|
42
|
+
latest[key] = value
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
value !== null &&
|
|
46
|
+
typeof value === "object" &&
|
|
47
|
+
!childListeners.has(key)
|
|
48
|
+
) {
|
|
49
|
+
const childApi = chain().next(key)
|
|
50
|
+
const handler = childData => {
|
|
51
|
+
if (childData === null) {
|
|
52
|
+
delete latest[key]
|
|
53
|
+
childListeners.delete(key)
|
|
54
|
+
} else {
|
|
55
|
+
latest[key] = childData
|
|
56
|
+
}
|
|
57
|
+
notify()
|
|
58
|
+
}
|
|
59
|
+
childListeners.set(key, {api: childApi, handler})
|
|
60
|
+
// false, not true - the current value is already in data above,
|
|
61
|
+
// so this only needs to watch for future changes, not trigger a
|
|
62
|
+
// redundant immediate re-read.
|
|
63
|
+
childApi.on(handler, false, opt)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const key of [...childListeners.keys()]) {
|
|
68
|
+
if (!(key in data)) {
|
|
69
|
+
const {api: childApi, handler} = childListeners.get(key)
|
|
70
|
+
childApi.off(handler)
|
|
71
|
+
childListeners.delete(key)
|
|
72
|
+
delete latest[key]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
notify()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const topLevelApi = chain()
|
|
80
|
+
topLevelApi.on(topLevelHandler, _get, opt)
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
topLevelApi.off(topLevelHandler)
|
|
84
|
+
for (const {api: childApi, handler} of childListeners.values()) {
|
|
85
|
+
childApi.off(handler)
|
|
86
|
+
}
|
|
87
|
+
childListeners.clear()
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/wire.js
CHANGED
|
@@ -452,14 +452,18 @@ const Wire = opt => {
|
|
|
452
452
|
continue
|
|
453
453
|
}
|
|
454
454
|
const limit = userLimits.get(pub) ?? defaultLimit
|
|
455
|
-
|
|
456
|
-
toStore[soul] = node
|
|
455
|
+
const current = userStorage.get(pub) ?? 0
|
|
457
456
|
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
458
457
|
const newBytes = graph[soul] ? JSON.stringify(graph[soul]).length : 0
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
458
|
+
const updated = Math.max(0, current - oldBytes + newBytes)
|
|
459
|
+
// A write that doesn't grow this user's total usage (e.g. a
|
|
460
|
+
// delete) is always let through, even past the limit - otherwise
|
|
461
|
+
// a user already over quota could never delete anything to get
|
|
462
|
+
// back under it. Only a write that both grows usage and pushes
|
|
463
|
+
// the total past the limit is rejected.
|
|
464
|
+
if (updated > current && updated > limit) continue
|
|
465
|
+
toStore[soul] = node
|
|
466
|
+
userStorage.set(pub, updated)
|
|
463
467
|
saveUserStorage()
|
|
464
468
|
}
|
|
465
469
|
if (Object.keys(toStore).length > 0) {
|
|
@@ -683,16 +687,22 @@ const Wire = opt => {
|
|
|
683
687
|
if (!pub) continue
|
|
684
688
|
const limit = userLimits.get(pub) ?? defaultLimit
|
|
685
689
|
const current = userStorage.get(pub) ?? 0
|
|
686
|
-
if (current >= limit) {
|
|
687
|
-
if (cb) cb("storage limit exceeded")
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
|
|
691
690
|
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
692
691
|
const newBytes = graph[soul]
|
|
693
692
|
? JSON.stringify(graph[soul]).length
|
|
694
693
|
: 0
|
|
695
|
-
|
|
694
|
+
const updated = Math.max(0, current - oldBytes + newBytes)
|
|
695
|
+
// A write that doesn't grow this user's total usage (e.g. a
|
|
696
|
+
// delete) is always let through, even past the limit - otherwise
|
|
697
|
+
// a user already over quota could never delete anything to get
|
|
698
|
+
// back under it. Only a write that both grows usage and pushes
|
|
699
|
+
// the total past the limit is rejected.
|
|
700
|
+
if (updated > current && updated > limit) {
|
|
701
|
+
if (cb) cb("storage limit exceeded")
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
userStorage.set(pub, updated)
|
|
696
706
|
saveUserStorage()
|
|
697
707
|
}
|
|
698
708
|
}
|
package/ts/src/holster.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as utils from "./utils.ts"
|
|
|
7
7
|
import Wire, {type WireAPI} from "./wire.ts"
|
|
8
8
|
import User, {type UserInterface} from "./user.ts"
|
|
9
9
|
import SEA from "./sea.ts"
|
|
10
|
+
import {attachNested} from "./nested.ts"
|
|
10
11
|
import type {
|
|
11
12
|
HolsterOptions,
|
|
12
13
|
ChainItem,
|
|
@@ -55,12 +56,15 @@ export interface HolsterAPI {
|
|
|
55
56
|
set?: boolean | ((data?: string | null) => void),
|
|
56
57
|
cb?: (data?: string | null) => void,
|
|
57
58
|
) => HolsterAPI | void
|
|
58
|
-
on:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
on: {
|
|
60
|
+
(
|
|
61
|
+
lex: LexWithDot,
|
|
62
|
+
cb: (data: unknown) => void,
|
|
63
|
+
_get?: boolean,
|
|
64
|
+
_opt?: WireOptions,
|
|
65
|
+
): void
|
|
66
|
+
(cb: (data: unknown) => void, _get?: boolean, _opt?: WireOptions): void
|
|
67
|
+
}
|
|
64
68
|
off: (cb?: (data: unknown) => void) => HolsterAPI | void
|
|
65
69
|
user: () => UserInterface & HolsterAPI
|
|
66
70
|
wire: WireAPI
|
|
@@ -78,6 +82,12 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
78
82
|
const user = User(options, wire as never)
|
|
79
83
|
const map = new Map<(data: unknown) => void, () => void>()
|
|
80
84
|
const allctx = new Map<string, ApiContext>()
|
|
85
|
+
// Tracks on(..., {nested: true}) subscriptions, keyed by the caller's
|
|
86
|
+
// own callback (the same reference off() will be called with) - see
|
|
87
|
+
// attachNested in nested.ts. The actual top-level/child listeners it
|
|
88
|
+
// creates use their own, separate contexts, so off() just needs to
|
|
89
|
+
// call the detach function this stores to tear the whole thing down.
|
|
90
|
+
const nested = new Map<(data: unknown) => void, () => void>()
|
|
81
91
|
// Serializes concurrent creation of a missing rel for the same
|
|
82
92
|
// soul+item, so only the first caller creates it and the rest reuse
|
|
83
93
|
// its result rather than each minting a competing soul. Map<soul,
|
|
@@ -884,6 +894,23 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
884
894
|
|
|
885
895
|
const ctx = allctx.get(ctxid)
|
|
886
896
|
|
|
897
|
+
if (opts && opts.nested) {
|
|
898
|
+
// attachNested calls this once for the top level and again for
|
|
899
|
+
// every discovered child, each needing its own independent
|
|
900
|
+
// context - a single shared _ctxid would accumulate .next()
|
|
901
|
+
// calls onto the same path instead of giving sibling paths.
|
|
902
|
+
const factory = (): HolsterAPI => {
|
|
903
|
+
const _ctxid = utils.text.random()
|
|
904
|
+
allctx.set(_ctxid, {
|
|
905
|
+
chain: [{item: item, soul: soul}],
|
|
906
|
+
user: ctx ? ctx.user : null,
|
|
907
|
+
})
|
|
908
|
+
return api(_ctxid)
|
|
909
|
+
}
|
|
910
|
+
nested.set(callback, attachNested(factory, callback, _get, opts))
|
|
911
|
+
return
|
|
912
|
+
}
|
|
913
|
+
|
|
887
914
|
allctx.set(ctxid, {
|
|
888
915
|
chain: [{item: item, soul: soul}],
|
|
889
916
|
on: true,
|
|
@@ -1043,7 +1070,7 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
1043
1070
|
: options.secure,
|
|
1044
1071
|
},
|
|
1045
1072
|
)
|
|
1046
|
-
},
|
|
1073
|
+
} as HolsterAPI["on"],
|
|
1047
1074
|
|
|
1048
1075
|
off: function (cb?: (data: unknown) => void): HolsterAPI | void {
|
|
1049
1076
|
if (!ctxid) {
|
|
@@ -1052,6 +1079,12 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
1052
1079
|
return
|
|
1053
1080
|
}
|
|
1054
1081
|
|
|
1082
|
+
if (cb && nested.has(cb)) {
|
|
1083
|
+
nested.get(cb)!()
|
|
1084
|
+
nested.delete(cb)
|
|
1085
|
+
return
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1055
1088
|
const resolved = resolve({off: true}, cb)
|
|
1056
1089
|
if (!resolved) return
|
|
1057
1090
|
|
package/ts/src/nested.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {HolsterAPI} from "./holster.ts"
|
|
2
|
+
import type {WireOptions} from "./schemas.ts"
|
|
3
|
+
|
|
4
|
+
// Backs on(cb, _get, {nested: true}) - see holster.ts. A plain on() only
|
|
5
|
+
// resolves the node it's given; it never re-checks a child whose own
|
|
6
|
+
// value is an object (which always has its own soul, since only scalars
|
|
7
|
+
// live directly on a node) once that child's content changes in place -
|
|
8
|
+
// nothing on the parent's own soul changes when that happens, so a
|
|
9
|
+
// listener on the parent alone never sees it. get()'s own read-resolution
|
|
10
|
+
// (holster.ts) already recursively resolves every such child into plain
|
|
11
|
+
// data for a one-time read - this doesn't repeat that; it adds a live
|
|
12
|
+
// subscription for each child that resolution already exposed, so a
|
|
13
|
+
// later change to it reaches the caller too.
|
|
14
|
+
//
|
|
15
|
+
// Pure and holster-internals-free: chain is a factory - () =>
|
|
16
|
+
// user.get([host, "shared"]).next("domains").next(code) - not a
|
|
17
|
+
// pre-built chained object, since a fresh, independent instance is
|
|
18
|
+
// needed per discovered child (reusing one chained object across
|
|
19
|
+
// multiple .next() calls would push them onto the same path instead of
|
|
20
|
+
// giving sibling paths).
|
|
21
|
+
export function attachNested(
|
|
22
|
+
chain: () => HolsterAPI,
|
|
23
|
+
cb: (data: unknown) => void,
|
|
24
|
+
_get?: boolean,
|
|
25
|
+
_opt?: WireOptions,
|
|
26
|
+
): () => void {
|
|
27
|
+
// Stripped before being used on any .on() call this function makes
|
|
28
|
+
// itself (top-level or child) - otherwise a caller-provided {nested:
|
|
29
|
+
// true} would re-trigger holster.ts's own nested check on re-entry,
|
|
30
|
+
// before the normal rel-resolution logic that actually finds the right
|
|
31
|
+
// soul ever runs.
|
|
32
|
+
const opt: WireOptions = {..._opt}
|
|
33
|
+
delete opt.nested
|
|
34
|
+
|
|
35
|
+
const childListeners = new Map<
|
|
36
|
+
string,
|
|
37
|
+
{api: HolsterAPI; handler: (data: unknown) => void}
|
|
38
|
+
>()
|
|
39
|
+
let latest: Record<string, unknown> | null = null
|
|
40
|
+
|
|
41
|
+
const notify = () => cb(latest === null ? null : {...latest})
|
|
42
|
+
|
|
43
|
+
const topLevelHandler = (data: unknown) => {
|
|
44
|
+
if (data === null || typeof data !== "object") {
|
|
45
|
+
latest = data as Record<string, unknown> | null
|
|
46
|
+
notify()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!latest || typeof latest !== "object") latest = {}
|
|
51
|
+
const node = data as Record<string, unknown>
|
|
52
|
+
for (const [key, value] of Object.entries(node)) {
|
|
53
|
+
if (key === "_") continue
|
|
54
|
+
latest[key] = value
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof value === "object" &&
|
|
59
|
+
!childListeners.has(key)
|
|
60
|
+
) {
|
|
61
|
+
const childApi = chain().next(key)
|
|
62
|
+
const handler = (childData: unknown) => {
|
|
63
|
+
if (childData === null) {
|
|
64
|
+
delete latest![key]
|
|
65
|
+
childListeners.delete(key)
|
|
66
|
+
} else {
|
|
67
|
+
latest![key] = childData
|
|
68
|
+
}
|
|
69
|
+
notify()
|
|
70
|
+
}
|
|
71
|
+
childListeners.set(key, {api: childApi, handler})
|
|
72
|
+
// false, not true - the current value is already in data above,
|
|
73
|
+
// so this only needs to watch for future changes, not trigger a
|
|
74
|
+
// redundant immediate re-read.
|
|
75
|
+
childApi.on(handler, false, opt)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const key of [...childListeners.keys()]) {
|
|
80
|
+
if (!(key in node)) {
|
|
81
|
+
const entry = childListeners.get(key)!
|
|
82
|
+
entry.api.off(entry.handler)
|
|
83
|
+
childListeners.delete(key)
|
|
84
|
+
delete latest[key]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
notify()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const topLevelApi = chain()
|
|
92
|
+
topLevelApi.on(topLevelHandler, _get, opt)
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
topLevelApi.off(topLevelHandler)
|
|
96
|
+
for (const {api: childApi, handler} of childListeners.values()) {
|
|
97
|
+
childApi.off(handler)
|
|
98
|
+
}
|
|
99
|
+
childListeners.clear()
|
|
100
|
+
}
|
|
101
|
+
}
|
package/ts/src/schemas.ts
CHANGED
|
@@ -185,6 +185,7 @@ export const WireOptionsSchema = z
|
|
|
185
185
|
secure: z.boolean().optional(),
|
|
186
186
|
fast: z.boolean().optional(),
|
|
187
187
|
put: z.boolean().optional(),
|
|
188
|
+
nested: z.boolean().optional(),
|
|
188
189
|
})
|
|
189
190
|
.optional()
|
|
190
191
|
export type WireOptions = z.infer<typeof WireOptionsSchema>
|
package/ts/src/wire.ts
CHANGED
|
@@ -509,14 +509,18 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
509
509
|
continue
|
|
510
510
|
}
|
|
511
511
|
const limit = userLimits.get(pub) ?? defaultLimit
|
|
512
|
-
|
|
513
|
-
toStore[soul] = node
|
|
512
|
+
const current = userStorage.get(pub) ?? 0
|
|
514
513
|
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
515
514
|
const newBytes = graph[soul] ? JSON.stringify(graph[soul]).length : 0
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
515
|
+
const updated = Math.max(0, current - oldBytes + newBytes)
|
|
516
|
+
// A write that doesn't grow this user's total usage (e.g. a
|
|
517
|
+
// delete) is always let through, even past the limit - otherwise
|
|
518
|
+
// a user already over quota could never delete anything to get
|
|
519
|
+
// back under it. Only a write that both grows usage and pushes
|
|
520
|
+
// the total past the limit is rejected.
|
|
521
|
+
if (updated > current && updated > limit) continue
|
|
522
|
+
toStore[soul] = node
|
|
523
|
+
userStorage.set(pub, updated)
|
|
520
524
|
saveUserStorage()
|
|
521
525
|
}
|
|
522
526
|
if (Object.keys(toStore).length > 0) {
|
|
@@ -730,15 +734,21 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
730
734
|
if (!pub) continue
|
|
731
735
|
const limit = userLimits.get(pub) ?? defaultLimit
|
|
732
736
|
const current = userStorage.get(pub) ?? 0
|
|
733
|
-
if (current >= limit) {
|
|
734
|
-
if (cb) cb("storage limit exceeded")
|
|
735
|
-
return
|
|
736
|
-
}
|
|
737
737
|
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
738
738
|
const newBytes = graph[soul]
|
|
739
739
|
? JSON.stringify(graph[soul]).length
|
|
740
740
|
: 0
|
|
741
|
-
|
|
741
|
+
const updated = Math.max(0, current - oldBytes + newBytes)
|
|
742
|
+
// A write that doesn't grow this user's total usage (e.g. a
|
|
743
|
+
// delete) is always let through, even past the limit -
|
|
744
|
+
// otherwise a user already over quota could never delete
|
|
745
|
+
// anything to get back under it. Only a write that both grows
|
|
746
|
+
// usage and pushes the total past the limit is rejected.
|
|
747
|
+
if (updated > current && updated > limit) {
|
|
748
|
+
if (cb) cb("storage limit exceeded")
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
userStorage.set(pub, updated)
|
|
742
752
|
saveUserStorage()
|
|
743
753
|
}
|
|
744
754
|
}
|