@wovin/core 0.1.35 → 0.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/README.md +0 -12
- package/dist/applog/applog-helpers.d.ts +12 -12
- package/dist/applog/applog-helpers.d.ts.map +1 -1
- package/dist/applog/applog-utils.d.ts +25 -6
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts +4 -5
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.d.ts +3 -3
- package/dist/applog.d.ts.map +1 -1
- package/dist/{applog.min.js → applog.js} +6 -7
- package/dist/blockstore.d.ts +1 -1
- package/dist/blockstore.d.ts.map +1 -1
- package/dist/{blockstore.min.js → blockstore.js} +1 -3
- package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
- package/dist/{chunk-KXMTKPF4.min.js → chunk-3JZMOEOD.js} +8 -8
- package/dist/chunk-3JZMOEOD.js.map +1 -0
- package/dist/chunk-3WZVG277.js +434 -0
- package/dist/chunk-3WZVG277.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-CPSDKFBG.js +147 -0
- package/dist/chunk-CPSDKFBG.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/{chunk-H3VQJP56.min.js → chunk-J2FDHGOZ.js} +9 -9
- package/dist/chunk-J2FDHGOZ.js.map +1 -0
- package/dist/chunk-L5EEEGE6.js +1862 -0
- package/dist/chunk-L5EEEGE6.js.map +1 -0
- package/dist/{chunk-BRC7LSM6.min.js → chunk-PD3C7XUM.js} +5 -5
- package/dist/chunk-PD3C7XUM.js.map +1 -0
- package/dist/chunk-QZXKQCAY.js +1026 -0
- package/dist/chunk-QZXKQCAY.js.map +1 -0
- package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/{index.min.js → index.js} +73 -46
- package/dist/ipfs/car.d.ts +11 -11
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs/ipfs-utils.d.ts +2 -2
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
- package/dist/ipfs.d.ts +3 -3
- package/dist/ipfs.d.ts.map +1 -1
- package/dist/{ipfs.min.js → ipfs.js} +7 -10
- package/dist/ipns.d.ts +1 -1
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +3 -3
- package/dist/pubsub/pub-pull.d.ts.map +1 -1
- package/dist/pubsub/pubsub-types.d.ts +3 -3
- package/dist/pubsub/pubsub-types.d.ts.map +1 -1
- package/dist/pubsub/snap-push.d.ts +4 -4
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub/ucan.d.ts +1 -1
- package/dist/pubsub/ucan.d.ts.map +1 -1
- package/dist/pubsub.d.ts +4 -4
- package/dist/pubsub.d.ts.map +1 -1
- package/dist/{pubsub.min.js → pubsub.js} +7 -10
- package/dist/query/attr-helpers.d.ts +5 -0
- package/dist/query/attr-helpers.d.ts.map +1 -0
- package/dist/query/basic.d.ts +85 -21
- package/dist/query/basic.d.ts.map +1 -1
- package/dist/query/divergences.d.ts +5 -5
- package/dist/query/divergences.d.ts.map +1 -1
- package/dist/query/entity-collection.d.ts +19 -0
- package/dist/query/entity-collection.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +1 -1
- package/dist/query/matchers.d.ts.map +1 -1
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/situations.d.ts +2 -1
- package/dist/query/situations.d.ts.map +1 -1
- package/dist/query/subscribable.d.ts +111 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +54 -14
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query.d.ts +9 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/{query.min.js → query.js} +51 -32
- package/dist/retrieve/index.d.ts +1 -1
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/update-thread.d.ts +3 -3
- package/dist/retrieve/update-thread.d.ts.map +1 -1
- package/dist/retrieve.d.ts +1 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +14 -0
- package/dist/thread/basic.d.ts +15 -19
- package/dist/thread/basic.d.ts.map +1 -1
- package/dist/thread/filters.d.ts +8 -10
- package/dist/thread/filters.d.ts.map +1 -1
- package/dist/thread/indexes.d.ts +56 -0
- package/dist/thread/indexes.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +40 -11
- package/dist/thread/mapped.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +5 -5
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/writeable.d.ts +2 -2
- package/dist/thread/writeable.d.ts.map +1 -1
- package/dist/thread.d.ts +6 -5
- package/dist/thread.d.ts.map +1 -1
- package/dist/{thread.min.js → thread.js} +9 -6
- package/dist/types/typescript-utils.d.ts +6 -5
- package/dist/types/typescript-utils.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{types.min.js → types.js} +3 -4
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/package.json +32 -23
- package/src/applog/applog-helpers.ts +155 -0
- package/src/applog/applog-utils.test.ts +108 -0
- package/src/applog/applog-utils.ts +507 -0
- package/src/applog/datom-types.ts +148 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +277 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/attr-helpers.ts +5 -0
- package/src/query/basic.ts +1245 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/entity-collection.ts +131 -0
- package/src/query/liveFilterAndMap.test.ts +102 -0
- package/src/query/matchers.ts +8 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +538 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +234 -0
- package/src/query/types.ts +155 -0
- package/src/query/withoutDeleted.test.ts +204 -0
- package/src/query.ts +9 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +182 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +227 -0
- package/src/thread/indexes.ts +250 -0
- package/src/thread/joinThreads.test.ts +304 -0
- package/src/thread/mapped.ts +226 -0
- package/src/thread/utils.ts +144 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +6 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- package/src/utils.ts +4 -0
- package/dist/chunk-2Y2PYHGR.min.js +0 -65
- package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
- package/dist/chunk-5MMGBK2U.min.js +0 -1
- package/dist/chunk-7IDQIMQO.min.js +0 -1
- package/dist/chunk-BRC7LSM6.min.js.map +0 -1
- package/dist/chunk-COXXILXC.min.js +0 -512
- package/dist/chunk-COXXILXC.min.js.map +0 -1
- package/dist/chunk-GDX2OO7L.min.js +0 -9080
- package/dist/chunk-GDX2OO7L.min.js.map +0 -1
- package/dist/chunk-H3VQJP56.min.js.map +0 -1
- package/dist/chunk-HYMC7W6S.min.js +0 -1549
- package/dist/chunk-HYMC7W6S.min.js.map +0 -1
- package/dist/chunk-KEHU7HGZ.min.js +0 -5216
- package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
- package/dist/chunk-KXMTKPF4.min.js.map +0 -1
- package/dist/chunk-PHITDXZT.min.js +0 -36
- package/dist/chunk-QO2KMGDN.min.js +0 -3771
- package/dist/chunk-QO2KMGDN.min.js.map +0 -1
- package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
- package/dist/chunk-WXLCBTHX.min.js +0 -1606
- package/dist/chunk-WXLCBTHX.min.js.map +0 -1
- package/dist/ipns.min.js +0 -6419
- package/dist/ipns.min.js.map +0 -1
- package/dist/mobx/mobx-utils.d.ts +0 -82
- package/dist/mobx/mobx-utils.d.ts.map +0 -1
- package/dist/mobx.d.ts +0 -2
- package/dist/mobx.d.ts.map +0 -1
- package/dist/mobx.min.js +0 -141
- package/dist/retrieve.min.js +0 -17
- package/dist/types.min.js.map +0 -1
- package/dist/utils.min.js +0 -10
- package/dist/utils.min.js.map +0 -1
- /package/dist/{applog.min.js.map → applog.js.map} +0 -0
- /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
- /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
- /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
- /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
- /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
- /package/dist/{mobx.min.js.map → query.js.map} +0 -0
- /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
- /package/dist/{query.min.js.map → thread.js.map} +0 -0
- /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
- /package/dist/{thread.min.js.map → utils.js.map} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import stringify from 'safe-stable-stringify'
|
|
3
|
+
import { Applog, CidString } from '../applog/datom-types.ts'
|
|
4
|
+
import { createDebugName } from '../utils/debug-name.ts'
|
|
5
|
+
import { Thread } from '../thread/basic.ts'
|
|
6
|
+
import { ThreadInMemory } from '../thread/writeable.ts'
|
|
7
|
+
import { memoizedFn } from './memoized.ts'
|
|
8
|
+
|
|
9
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
10
|
+
|
|
11
|
+
export interface DivergenceLeaf {
|
|
12
|
+
log: Applog
|
|
13
|
+
thread: Thread
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const queryDivergencesByPrev = memoizedFn('queryDivergencesByPrev', function queryConflictingByPrev(
|
|
17
|
+
sourceThread: Thread,
|
|
18
|
+
) {
|
|
19
|
+
DEBUG(`queryDivergencesByPrev<${sourceThread.nameAndSizeUntracked}>`)
|
|
20
|
+
if (sourceThread.filters.includes('lastWriteWins')) WARN(`queryDivergencesByPrev on thread lastWriteWins`, sourceThread)
|
|
21
|
+
|
|
22
|
+
const logsForNode = new Map<CidString, Applog[]>()
|
|
23
|
+
const leafs = new Set<CidString>()
|
|
24
|
+
VERBOSE('all applogs:', sourceThread.applogs)
|
|
25
|
+
for (const log of sourceThread.applogs) {
|
|
26
|
+
let prevLogs
|
|
27
|
+
if (log.pv) {
|
|
28
|
+
prevLogs = log.pv && logsForNode.get(log.pv.toString())
|
|
29
|
+
leafs.delete(log.pv.toString())
|
|
30
|
+
}
|
|
31
|
+
VERBOSE('traversing log', { log, prevLogs, leafs: Array.from(leafs) })
|
|
32
|
+
logsForNode.set(log.cid, prevLogs ? [...prevLogs, log] : [log])
|
|
33
|
+
leafs.add(log.cid)
|
|
34
|
+
}
|
|
35
|
+
const divergences = Array.from(leafs).map(leafID => {
|
|
36
|
+
const thread = new ThreadInMemory(
|
|
37
|
+
createDebugName({
|
|
38
|
+
caller: 'DivergenceLeaf',
|
|
39
|
+
thread: sourceThread,
|
|
40
|
+
pattern: `leaf: ${leafID}`,
|
|
41
|
+
}),
|
|
42
|
+
logsForNode.get(leafID),
|
|
43
|
+
sourceThread.filters,
|
|
44
|
+
true,
|
|
45
|
+
)
|
|
46
|
+
return ({ log: thread.latestLog, thread })
|
|
47
|
+
})
|
|
48
|
+
// TODO: migrate to SubscribableArray for reactive updates
|
|
49
|
+
return divergences
|
|
50
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import { Applog, ApplogValue, DatalogQueryPattern, EntityID } from '../applog/datom-types.ts'
|
|
3
|
+
import { isInitEvent, Thread } from '../thread/basic.ts'
|
|
4
|
+
import { makeFilter, rollingFilter } from '../thread/filters.ts'
|
|
5
|
+
import { resolveKeyMapper } from './basic.ts'
|
|
6
|
+
import { memoizedFn } from './memoized.ts'
|
|
7
|
+
import { SubscribableImpl } from './subscribable.ts'
|
|
8
|
+
import type { Subscribable } from './subscribable.ts'
|
|
9
|
+
import type { StripExplicitPrefix, StripFirstPrefix } from './attr-helpers.ts'
|
|
10
|
+
|
|
11
|
+
const { DEBUG } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
12
|
+
|
|
13
|
+
export function liveEntityCollection<A extends string>(
|
|
14
|
+
thread: Thread, discoveryPattern: DatalogQueryPattern, liveAttributes: readonly A[],
|
|
15
|
+
): Subscribable<ReadonlyMap<EntityID, Record<A, ApplogValue | null>>>
|
|
16
|
+
export function liveEntityCollection<A extends string>(
|
|
17
|
+
thread: Thread, discoveryPattern: DatalogQueryPattern, liveAttributes: readonly A[],
|
|
18
|
+
opts: { stripAtPrefix: true },
|
|
19
|
+
): Subscribable<ReadonlyMap<EntityID, Record<StripFirstPrefix<A>, ApplogValue | null>>>
|
|
20
|
+
export function liveEntityCollection<A extends string, P extends string>(
|
|
21
|
+
thread: Thread, discoveryPattern: DatalogQueryPattern, liveAttributes: readonly A[],
|
|
22
|
+
opts: { stripAtPrefix: P },
|
|
23
|
+
): Subscribable<ReadonlyMap<EntityID, Record<StripExplicitPrefix<A, P>, ApplogValue | null>>>
|
|
24
|
+
export function liveEntityCollection<A extends string>(
|
|
25
|
+
thread: Thread, discoveryPattern: DatalogQueryPattern, liveAttributes: readonly A[],
|
|
26
|
+
opts: { mapKeys: (attr: A) => string },
|
|
27
|
+
): Subscribable<ReadonlyMap<EntityID, Record<string, ApplogValue | null>>>
|
|
28
|
+
export function liveEntityCollection<A extends string>(
|
|
29
|
+
thread: Thread, discoveryPattern: DatalogQueryPattern, liveAttributes: readonly A[],
|
|
30
|
+
opts: { stripAtPrefix?: true | string; mapKeys?: (attr: A) => string },
|
|
31
|
+
): Subscribable<ReadonlyMap<EntityID, Record<string, ApplogValue | null>>>
|
|
32
|
+
export function liveEntityCollection<A extends string>(
|
|
33
|
+
thread: Thread,
|
|
34
|
+
discoveryPattern: DatalogQueryPattern,
|
|
35
|
+
liveAttributes: readonly A[],
|
|
36
|
+
opts?: { stripAtPrefix?: true | string; mapKeys?: (attr: A) => string },
|
|
37
|
+
): Subscribable<ReadonlyMap<EntityID, Record<string, ApplogValue | null>>> {
|
|
38
|
+
return _liveEntityCollection(thread, discoveryPattern, liveAttributes,
|
|
39
|
+
opts as { stripAtPrefix?: true | string; mapKeys?: (attr: string) => string })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const _liveEntityCollection = memoizedFn('liveEntityCollection',
|
|
43
|
+
function liveEntityCollection<A extends string>(
|
|
44
|
+
thread: Thread,
|
|
45
|
+
discoveryPattern: DatalogQueryPattern,
|
|
46
|
+
liveAttributes: readonly A[],
|
|
47
|
+
opts?: { stripAtPrefix?: true | string; mapKeys?: (attr: string) => string },
|
|
48
|
+
): Subscribable<ReadonlyMap<EntityID, Record<string, ApplogValue | null>>> {
|
|
49
|
+
DEBUG('liveEntityCollection', discoveryPattern, liveAttributes)
|
|
50
|
+
const discoveryAttr = discoveryPattern.at as string
|
|
51
|
+
const allAttrs = new Set([discoveryAttr, ...liveAttributes])
|
|
52
|
+
const filtered = rollingFilter(thread, { at: [...allAttrs] })
|
|
53
|
+
const isDiscoveryMatch = makeFilter(discoveryPattern)
|
|
54
|
+
const attrSet = new Set<string>(liveAttributes)
|
|
55
|
+
const key = resolveKeyMapper(opts)
|
|
56
|
+
|
|
57
|
+
const map = new Map<EntityID, Record<string, ApplogValue | null>>()
|
|
58
|
+
|
|
59
|
+
function makeRecord(entityId: EntityID): Record<string, ApplogValue | null> {
|
|
60
|
+
const record = {} as Record<string, ApplogValue | null>
|
|
61
|
+
for (const attr of liveAttributes) record[key(attr)] = null
|
|
62
|
+
// Backfill from current filtered state
|
|
63
|
+
for (const log of filtered.applogs) {
|
|
64
|
+
if (log.en === entityId && attrSet.has(log.at)) {
|
|
65
|
+
record[key(log.at)] = log.vl
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return record
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildFull(applogs: readonly Applog[]) {
|
|
72
|
+
map.clear()
|
|
73
|
+
for (const log of isDiscoveryMatch(applogs)) {
|
|
74
|
+
if (!map.has(log.en)) map.set(log.en, makeRecord(log.en))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addLog(log: Applog) {
|
|
79
|
+
// Discovery match → ensure entity exists
|
|
80
|
+
if (isDiscoveryMatch([log]).length > 0 && !map.has(log.en)) {
|
|
81
|
+
map.set(log.en, makeRecord(log.en))
|
|
82
|
+
return // makeRecord already backfilled attrs
|
|
83
|
+
}
|
|
84
|
+
// Attribute match → update value
|
|
85
|
+
if (attrSet.has(log.at)) {
|
|
86
|
+
const record = map.get(log.en)
|
|
87
|
+
if (record) record[key(log.at)] = log.vl
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function removeLog(log: Applog) {
|
|
92
|
+
if (isDiscoveryMatch([log]).length > 0) {
|
|
93
|
+
// Check if entity still has another discovery match
|
|
94
|
+
const stillDiscovered = filtered.applogs.some(
|
|
95
|
+
l => l.en === log.en && isDiscoveryMatch([l]).length > 0,
|
|
96
|
+
)
|
|
97
|
+
if (!stillDiscovered) {
|
|
98
|
+
map.delete(log.en)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (attrSet.has(log.at)) {
|
|
103
|
+
const record = map.get(log.en)
|
|
104
|
+
if (record) {
|
|
105
|
+
// Find current value from remaining applogs
|
|
106
|
+
const current = filtered.applogs.find(l => l.en === log.en && l.at === log.at)
|
|
107
|
+
record[key(log.at)] = current?.vl ?? null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Initial build
|
|
113
|
+
buildFull(filtered.applogs)
|
|
114
|
+
|
|
115
|
+
const result = new SubscribableImpl<ReadonlyMap<EntityID, Record<string, ApplogValue | null>>>(
|
|
116
|
+
map,
|
|
117
|
+
() => filtered.subscribe((event) => {
|
|
118
|
+
if (isInitEvent(event)) {
|
|
119
|
+
buildFull(event.init)
|
|
120
|
+
} else {
|
|
121
|
+
// Process removes before adds — LWW updates appear as remove+add in same delta
|
|
122
|
+
if (event.removed) for (const log of event.removed) removeLog(log)
|
|
123
|
+
for (const log of event.added) addLog(log)
|
|
124
|
+
}
|
|
125
|
+
result._set(map)
|
|
126
|
+
}, 'derived'),
|
|
127
|
+
{ equals: false },
|
|
128
|
+
)
|
|
129
|
+
return result
|
|
130
|
+
},
|
|
131
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { finalizeApplogForInsert } from '../applog/applog-helpers.ts'
|
|
3
|
+
import { sortApplogsByTs } from '../applog/applog-utils.ts'
|
|
4
|
+
import type { Applog, ApplogForInsert } from '../applog/datom-types.ts'
|
|
5
|
+
import { ThreadInMemory } from '../thread/writeable.ts'
|
|
6
|
+
import { lastWriteWins, liveFilterAndMap } from './basic.ts'
|
|
7
|
+
|
|
8
|
+
let tsCounter = 0
|
|
9
|
+
function makeApplogs(inputs: ApplogForInsert[]): Applog[] {
|
|
10
|
+
const logs = inputs.map(input =>
|
|
11
|
+
finalizeApplogForInsert({
|
|
12
|
+
ts: new Date(1700000000000 + ++tsCounter * 1000).toISOString(),
|
|
13
|
+
pv: null,
|
|
14
|
+
ag: 'testAgent',
|
|
15
|
+
...input,
|
|
16
|
+
} as ApplogForInsert, {}),
|
|
17
|
+
)
|
|
18
|
+
sortApplogsByTs(logs)
|
|
19
|
+
return logs
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let db: ReturnType<typeof ThreadInMemory.fromArray>
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
db = ThreadInMemory.fromArray(
|
|
26
|
+
makeApplogs([
|
|
27
|
+
{ en: 'p1', at: 'person/name', vl: 'Alice', ag: 'testAgent' },
|
|
28
|
+
{ en: 'p2', at: 'person/name', vl: 'Bob', ag: 'testAgent' },
|
|
29
|
+
]),
|
|
30
|
+
'test-people',
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('liveFilterAndMap — identity tracking', () => {
|
|
35
|
+
it('reflects added items via _push', () => {
|
|
36
|
+
const lww = lastWriteWins(db)
|
|
37
|
+
const result = liveFilterAndMap(lww, { at: 'person/name' }, 'vl')
|
|
38
|
+
const unsub = result.subscribe(() => {})
|
|
39
|
+
|
|
40
|
+
expect([...result.items].sort()).toEqual(['Alice', 'Bob'])
|
|
41
|
+
|
|
42
|
+
db.insert([{ en: 'p3', at: 'person/name', vl: 'Carol', ag: 'testAgent' }])
|
|
43
|
+
|
|
44
|
+
expect([...result.items].sort()).toEqual(['Alice', 'Bob', 'Carol'])
|
|
45
|
+
unsub()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reflects removed items via _remove (LWW supersedes)', () => {
|
|
49
|
+
const lww = lastWriteWins(db)
|
|
50
|
+
const result = liveFilterAndMap(lww, { at: 'person/name' }, 'vl')
|
|
51
|
+
const unsub = result.subscribe(() => {})
|
|
52
|
+
|
|
53
|
+
expect([...result.items].sort()).toEqual(['Alice', 'Bob'])
|
|
54
|
+
|
|
55
|
+
// Supersede p1's name — LWW emits removed: [oldP1], added: [newP1]
|
|
56
|
+
db.insert([{ en: 'p1', at: 'person/name', vl: 'Alicia', ag: 'testAgent' }])
|
|
57
|
+
|
|
58
|
+
expect([...result.items].sort()).toEqual(['Alicia', 'Bob'])
|
|
59
|
+
unsub()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('handles successive supersedes (no leak in identity index)', () => {
|
|
63
|
+
const lww = lastWriteWins(db)
|
|
64
|
+
const result = liveFilterAndMap(lww, { at: 'person/name' }, 'vl')
|
|
65
|
+
const unsub = result.subscribe(() => {})
|
|
66
|
+
|
|
67
|
+
db.insert([{ en: 'p1', at: 'person/name', vl: 'Alicia', ag: 'testAgent' }])
|
|
68
|
+
db.insert([{ en: 'p1', at: 'person/name', vl: 'Alex', ag: 'testAgent' }])
|
|
69
|
+
db.insert([{ en: 'p2', at: 'person/name', vl: 'Bobby', ag: 'testAgent' }])
|
|
70
|
+
|
|
71
|
+
expect([...result.items].sort()).toEqual(['Alex', 'Bobby'])
|
|
72
|
+
unsub()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('mapper as object form — removed routes through cidToMapped', () => {
|
|
76
|
+
const lww = lastWriteWins(db)
|
|
77
|
+
const result = liveFilterAndMap(lww, { at: 'person/name' }, { en: 'id', vl: 'name' })
|
|
78
|
+
const unsub = result.subscribe(() => {})
|
|
79
|
+
|
|
80
|
+
expect(result.items).toHaveLength(2)
|
|
81
|
+
expect(result.items.find(r => r.id === 'p1')?.name).toBe('Alice')
|
|
82
|
+
|
|
83
|
+
db.insert([{ en: 'p1', at: 'person/name', vl: 'Alicia', ag: 'testAgent' }])
|
|
84
|
+
|
|
85
|
+
expect(result.items).toHaveLength(2)
|
|
86
|
+
expect(result.items.find(r => r.id === 'p1')?.name).toBe('Alicia')
|
|
87
|
+
unsub()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('mapper as function form — removed routes through cidToMapped', () => {
|
|
91
|
+
const lww = lastWriteWins(db)
|
|
92
|
+
const result = liveFilterAndMap(lww, { at: 'person/name' }, log => `${log.en}=${log.vl}`)
|
|
93
|
+
const unsub = result.subscribe(() => {})
|
|
94
|
+
|
|
95
|
+
expect([...result.items].sort()).toEqual(['p1=Alice', 'p2=Bob'])
|
|
96
|
+
|
|
97
|
+
db.insert([{ en: 'p1', at: 'person/name', vl: 'Alicia', ag: 'testAgent' }])
|
|
98
|
+
|
|
99
|
+
expect([...result.items].sort()).toEqual(['p1=Alicia', 'p2=Bob'])
|
|
100
|
+
unsub()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { memoizedFn, refCountedMemoizedFn } from './memoized.ts'
|
|
3
|
+
|
|
4
|
+
describe('memoizedFn', () => {
|
|
5
|
+
it('returns cached result for same args', () => {
|
|
6
|
+
const fn = vi.fn((a: number, b: number) => a + b)
|
|
7
|
+
const memo = memoizedFn('add', fn)
|
|
8
|
+
|
|
9
|
+
expect(memo(1, 2)).toBe(3)
|
|
10
|
+
expect(memo(1, 2)).toBe(3)
|
|
11
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('computes fresh result for different args', () => {
|
|
15
|
+
const fn = vi.fn((a: number) => a * 2)
|
|
16
|
+
const memo = memoizedFn('double', fn)
|
|
17
|
+
|
|
18
|
+
expect(memo(3)).toBe(6)
|
|
19
|
+
expect(memo(5)).toBe(10)
|
|
20
|
+
expect(fn).toHaveBeenCalledTimes(2)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('deep-compares non-Thread args', () => {
|
|
24
|
+
const fn = vi.fn((obj: { x: number }) => obj.x * 2)
|
|
25
|
+
const memo = memoizedFn('deep', fn)
|
|
26
|
+
|
|
27
|
+
expect(memo({ x: 3 })).toBe(6)
|
|
28
|
+
expect(memo({ x: 3 })).toBe(6) // new object, same shape
|
|
29
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('evicts oldest entry when maxSize exceeded', () => {
|
|
33
|
+
const fn = vi.fn((n: number) => n * 10)
|
|
34
|
+
const memo = memoizedFn('lru', fn, { maxSize: 2 })
|
|
35
|
+
|
|
36
|
+
memo(1) // cache: [1]
|
|
37
|
+
memo(2) // cache: [1, 2]
|
|
38
|
+
memo(3) // cache: [2, 3] — evicts 1
|
|
39
|
+
|
|
40
|
+
expect(fn).toHaveBeenCalledTimes(3)
|
|
41
|
+
|
|
42
|
+
// 2 and 3 are cached
|
|
43
|
+
memo(2)
|
|
44
|
+
memo(3)
|
|
45
|
+
expect(fn).toHaveBeenCalledTimes(3)
|
|
46
|
+
|
|
47
|
+
// 1 was evicted, needs recomputation
|
|
48
|
+
memo(1)
|
|
49
|
+
expect(fn).toHaveBeenCalledTimes(4)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('refCountedMemoizedFn', () => {
|
|
54
|
+
it('returns same result for same args, increments refCount', () => {
|
|
55
|
+
const fn = vi.fn((n: number) => ({ value: n * 2 }))
|
|
56
|
+
const memo = refCountedMemoizedFn('rc', fn)
|
|
57
|
+
|
|
58
|
+
const ref1 = memo(5)
|
|
59
|
+
const ref2 = memo(5)
|
|
60
|
+
expect(ref1.value).toBe(ref2.value) // same object
|
|
61
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('calls onCleanup when last ref released', () => {
|
|
65
|
+
const cleanup = vi.fn()
|
|
66
|
+
const fn = vi.fn((n: number) => n * 2)
|
|
67
|
+
const memo = refCountedMemoizedFn('rc', fn, { onCleanup: cleanup })
|
|
68
|
+
|
|
69
|
+
const ref1 = memo(5)
|
|
70
|
+
const ref2 = memo(5)
|
|
71
|
+
|
|
72
|
+
ref1.release()
|
|
73
|
+
expect(cleanup).not.toHaveBeenCalled()
|
|
74
|
+
|
|
75
|
+
ref2.release()
|
|
76
|
+
expect(cleanup).toHaveBeenCalledOnce()
|
|
77
|
+
expect(cleanup).toHaveBeenCalledWith(10, 5) // result, ...args
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('recomputes after full cleanup', () => {
|
|
81
|
+
const fn = vi.fn((n: number) => n * 2)
|
|
82
|
+
const memo = refCountedMemoizedFn('rc', fn, { onCleanup: () => {} })
|
|
83
|
+
|
|
84
|
+
const ref1 = memo(3)
|
|
85
|
+
ref1.release()
|
|
86
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
87
|
+
|
|
88
|
+
const ref2 = memo(3)
|
|
89
|
+
expect(fn).toHaveBeenCalledTimes(2)
|
|
90
|
+
ref2.release()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('release is idempotent', () => {
|
|
94
|
+
const cleanup = vi.fn()
|
|
95
|
+
const fn = vi.fn((n: number) => n * 2)
|
|
96
|
+
const memo = refCountedMemoizedFn('rc', fn, { onCleanup: cleanup })
|
|
97
|
+
|
|
98
|
+
const ref = memo(1)
|
|
99
|
+
ref.release()
|
|
100
|
+
ref.release() // second release is a no-op
|
|
101
|
+
expect(cleanup).toHaveBeenCalledOnce()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('grace period delays cleanup', () => {
|
|
105
|
+
vi.useFakeTimers()
|
|
106
|
+
const cleanup = vi.fn()
|
|
107
|
+
const fn = vi.fn((n: number) => n * 2)
|
|
108
|
+
const memo = refCountedMemoizedFn('rc', fn, {
|
|
109
|
+
onCleanup: cleanup,
|
|
110
|
+
gracePeriodMs: 100,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const ref = memo(5)
|
|
114
|
+
ref.release()
|
|
115
|
+
|
|
116
|
+
// Not cleaned up yet
|
|
117
|
+
expect(cleanup).not.toHaveBeenCalled()
|
|
118
|
+
|
|
119
|
+
// Re-acquire within grace period
|
|
120
|
+
const ref2 = memo(5)
|
|
121
|
+
expect(fn).toHaveBeenCalledTimes(1) // reused, not recomputed
|
|
122
|
+
|
|
123
|
+
vi.advanceTimersByTime(200)
|
|
124
|
+
expect(cleanup).not.toHaveBeenCalled() // still held by ref2
|
|
125
|
+
|
|
126
|
+
ref2.release()
|
|
127
|
+
vi.advanceTimersByTime(50)
|
|
128
|
+
expect(cleanup).not.toHaveBeenCalled() // still in grace
|
|
129
|
+
|
|
130
|
+
vi.advanceTimersByTime(60)
|
|
131
|
+
expect(cleanup).toHaveBeenCalledOnce()
|
|
132
|
+
|
|
133
|
+
vi.useRealTimers()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('separate entries for different args', () => {
|
|
137
|
+
const cleanup = vi.fn()
|
|
138
|
+
const fn = vi.fn((n: number) => n * 2)
|
|
139
|
+
const memo = refCountedMemoizedFn('rc', fn, { onCleanup: cleanup })
|
|
140
|
+
|
|
141
|
+
const refA = memo(1)
|
|
142
|
+
const refB = memo(2)
|
|
143
|
+
expect(fn).toHaveBeenCalledTimes(2)
|
|
144
|
+
|
|
145
|
+
refA.release()
|
|
146
|
+
expect(cleanup).toHaveBeenCalledWith(2, 1)
|
|
147
|
+
|
|
148
|
+
refB.release()
|
|
149
|
+
expect(cleanup).toHaveBeenCalledWith(4, 2)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoizedFn — Simple argument memoization without MobX.
|
|
3
|
+
*
|
|
4
|
+
* Replaces computedFnDeepCompare for query functions.
|
|
5
|
+
* With lazy subscribe (SubscribableArray), there's no urgency to clean up
|
|
6
|
+
* cached entries — unsubscribed entries are just plain arrays in memory.
|
|
7
|
+
*
|
|
8
|
+
* refCountedMemoizedFn — Ref-counted memoization for reactive results.
|
|
9
|
+
* Returns a wrapper that increments a ref count on each call and decrements
|
|
10
|
+
* on dispose(). When refCount hits 0, the entry is cleaned up (with optional
|
|
11
|
+
* grace period).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isEqual } from 'lodash-es'
|
|
15
|
+
import { Thread } from '../thread/basic.ts'
|
|
16
|
+
|
|
17
|
+
/** Deep-compare args, but Thread instances by identity (same as MobX version) */
|
|
18
|
+
function compareStructural(argsA: any[], argsB: any[], versionsA?: number[]): boolean {
|
|
19
|
+
if (argsA.length !== argsB.length) return false
|
|
20
|
+
for (let i = 0; i < argsA.length; i++) {
|
|
21
|
+
if (argsA[i] instanceof Thread) {
|
|
22
|
+
if (argsB[i] !== argsA[i]) return false
|
|
23
|
+
// Check if thread was mutated since the cache entry was created
|
|
24
|
+
if (versionsA && versionsA[i] !== undefined && argsB[i]._version !== versionsA[i]) return false
|
|
25
|
+
} else {
|
|
26
|
+
if (!isEqual(argsA[i], argsB[i])) return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Snapshot Thread._version for each Thread arg */
|
|
33
|
+
function snapshotVersions(args: any[]): number[] {
|
|
34
|
+
return args.map(a => a instanceof Thread ? a._version : undefined)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MemoizedFnOptions {
|
|
38
|
+
argsEqual?: (a: any[], b: any[]) => boolean
|
|
39
|
+
argsDebugName?: (...args: any[]) => string
|
|
40
|
+
/** Max cache entries. Oldest evicted when exceeded. Default: unlimited. */
|
|
41
|
+
maxSize?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function memoizedFn<T extends (...args: any[]) => any>(
|
|
45
|
+
name: string,
|
|
46
|
+
fn: T,
|
|
47
|
+
opts?: MemoizedFnOptions,
|
|
48
|
+
): T {
|
|
49
|
+
// TODO: cache grows unboundedly by default — see todo/2026-04-17_ciao-mobx/memoized-cache-cleanup.md for analysis
|
|
50
|
+
const cache: Array<{ args: any[]; versions: number[]; result: any }> = []
|
|
51
|
+
const argsEqual = opts?.argsEqual ?? compareStructural
|
|
52
|
+
const maxSize = opts?.maxSize ?? Infinity
|
|
53
|
+
|
|
54
|
+
return function (this: any, ...args: any[]) {
|
|
55
|
+
const existing = cache.find(entry => argsEqual(entry.args, args, entry.versions))
|
|
56
|
+
if (existing) return existing.result
|
|
57
|
+
|
|
58
|
+
const result = fn.apply(this, args)
|
|
59
|
+
|
|
60
|
+
if (cache.length >= maxSize) {
|
|
61
|
+
cache.shift() // evict oldest
|
|
62
|
+
}
|
|
63
|
+
cache.push({ args, versions: snapshotVersions(args), result })
|
|
64
|
+
return result
|
|
65
|
+
} as any
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════
|
|
69
|
+
// refCountedMemoizedFn — ref-counted cache with disposal
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
/** A result wrapper returned by refCountedMemoizedFn. Call release() when done. */
|
|
73
|
+
export interface RefCounted<T> {
|
|
74
|
+
readonly value: T
|
|
75
|
+
/** Decrement ref count. When it reaches 0, the cache entry is cleaned up. */
|
|
76
|
+
release(): void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface RefCountedOptions<T extends (...args: any[]) => any> {
|
|
80
|
+
argsEqual?: (a: any[], b: any[]) => boolean
|
|
81
|
+
argsDebugName?: (...args: any[]) => string
|
|
82
|
+
/**
|
|
83
|
+
* Called when the last reference is released and the entry is evicted.
|
|
84
|
+
* Use to tear down subscriptions, dispose SubscribableArrays, etc.
|
|
85
|
+
*/
|
|
86
|
+
onCleanup?: (result: ReturnType<T>, ...args: Parameters<T>) => void
|
|
87
|
+
/**
|
|
88
|
+
* Grace period in ms before actually cleaning up after refCount hits 0.
|
|
89
|
+
* If someone re-acquires within this window, the entry is reused.
|
|
90
|
+
* Default: 0 (immediate cleanup).
|
|
91
|
+
*/
|
|
92
|
+
gracePeriodMs?: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface CacheEntry<T> {
|
|
96
|
+
args: any[]
|
|
97
|
+
result: T
|
|
98
|
+
refCount: number
|
|
99
|
+
graceTimer: ReturnType<typeof setTimeout> | null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a memoized function whose results are ref-counted.
|
|
104
|
+
*
|
|
105
|
+
* Currently unused — doesn't fit liveQuery's recursive multi-step structure
|
|
106
|
+
* (API break, cache loss on pattern change without grace period).
|
|
107
|
+
* See todo/2026-04-17_ciao-mobx/memoized-cache-cleanup.md for full analysis.
|
|
108
|
+
*
|
|
109
|
+
* Each call returns `RefCounted<ReturnType<fn>>`. Multiple calls with the same
|
|
110
|
+
* args return the same cached result with an incremented ref count.
|
|
111
|
+
* When all holders call `release()`, the cache entry is cleaned up
|
|
112
|
+
* (subject to optional grace period).
|
|
113
|
+
*
|
|
114
|
+
* Usage:
|
|
115
|
+
* ```
|
|
116
|
+
* const getFiltered = refCountedMemoizedFn('rollingFilter', (thread, pattern) => {
|
|
117
|
+
* // ... expensive setup, returns MappedThread
|
|
118
|
+
* }, { onCleanup: (result) => result.dispose() })
|
|
119
|
+
*
|
|
120
|
+
* const ref = getFiltered(myThread, myPattern)
|
|
121
|
+
* // use ref.value (the MappedThread)
|
|
122
|
+
* ref.release() // decrement; when last holder releases, onCleanup fires
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function refCountedMemoizedFn<T extends (...args: any[]) => any>(
|
|
126
|
+
name: string,
|
|
127
|
+
fn: T,
|
|
128
|
+
opts?: RefCountedOptions<T>,
|
|
129
|
+
): (...args: Parameters<T>) => RefCounted<ReturnType<T>> {
|
|
130
|
+
const cache: CacheEntry<ReturnType<T>>[] = []
|
|
131
|
+
const argsEqual = opts?.argsEqual ?? compareStructural
|
|
132
|
+
const gracePeriodMs = opts?.gracePeriodMs ?? 0
|
|
133
|
+
|
|
134
|
+
function evict(entry: CacheEntry<ReturnType<T>>, args: any[]) {
|
|
135
|
+
const idx = cache.indexOf(entry)
|
|
136
|
+
if (idx >= 0) cache.splice(idx, 1)
|
|
137
|
+
opts?.onCleanup?.(entry.result, ...args as any)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return function (this: any, ...args: Parameters<T>): RefCounted<ReturnType<T>> {
|
|
141
|
+
let entry = cache.find(e => argsEqual(e.args, args))
|
|
142
|
+
|
|
143
|
+
if (entry) {
|
|
144
|
+
// Cancel pending grace-period cleanup
|
|
145
|
+
if (entry.graceTimer !== null) {
|
|
146
|
+
clearTimeout(entry.graceTimer)
|
|
147
|
+
entry.graceTimer = null
|
|
148
|
+
}
|
|
149
|
+
entry.refCount++
|
|
150
|
+
} else {
|
|
151
|
+
const result = fn.apply(this, args)
|
|
152
|
+
entry = { args, result, refCount: 1, graceTimer: null }
|
|
153
|
+
cache.push(entry)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const capturedEntry = entry
|
|
157
|
+
let released = false
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
get value() { return capturedEntry.result },
|
|
161
|
+
release() {
|
|
162
|
+
if (released) return // idempotent
|
|
163
|
+
released = true
|
|
164
|
+
capturedEntry.refCount--
|
|
165
|
+
|
|
166
|
+
if (capturedEntry.refCount <= 0) {
|
|
167
|
+
if (gracePeriodMs > 0) {
|
|
168
|
+
capturedEntry.graceTimer = setTimeout(() => {
|
|
169
|
+
if (capturedEntry.refCount <= 0) {
|
|
170
|
+
evict(capturedEntry, args)
|
|
171
|
+
}
|
|
172
|
+
}, gracePeriodMs)
|
|
173
|
+
} else {
|
|
174
|
+
evict(capturedEntry, args)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|