@wovin/core 0.0.0-ciao-mobx-955482e8
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/LICENSE +661 -0
- package/README.md +3 -0
- package/dist/applog/applog-helpers.d.ts +47 -0
- package/dist/applog/applog-helpers.d.ts.map +1 -0
- package/dist/applog/applog-utils.d.ts +57 -0
- package/dist/applog/applog-utils.d.ts.map +1 -0
- package/dist/applog/datom-types.d.ts +128 -0
- package/dist/applog/datom-types.d.ts.map +1 -0
- package/dist/applog.d.ts +4 -0
- package/dist/applog.d.ts.map +1 -0
- package/dist/applog.js +101 -0
- package/dist/applog.js.map +1 -0
- package/dist/blockstore/index.d.ts +21 -0
- package/dist/blockstore/index.d.ts.map +1 -0
- package/dist/blockstore.d.ts +2 -0
- package/dist/blockstore.d.ts.map +1 -0
- package/dist/blockstore.js +24 -0
- package/dist/blockstore.js.map +1 -0
- package/dist/chunk-6MQKRL6W.js +86 -0
- package/dist/chunk-6MQKRL6W.js.map +1 -0
- package/dist/chunk-7MW34UEO.js +40 -0
- package/dist/chunk-7MW34UEO.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-7Z5YDQKK.js.map +1 -0
- package/dist/chunk-CY4NLISM.js +144 -0
- package/dist/chunk-CY4NLISM.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/chunk-E46VTKTZ.js.map +1 -0
- package/dist/chunk-O43W7UW6.js +434 -0
- package/dist/chunk-O43W7UW6.js.map +1 -0
- package/dist/chunk-XIQSYEV3.js +1604 -0
- package/dist/chunk-XIQSYEV3.js.map +1 -0
- package/dist/chunk-XVGW4QC3.js +55 -0
- package/dist/chunk-XVGW4QC3.js.map +1 -0
- package/dist/chunk-YDAKBU6Q.js +9 -0
- 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/chunk-ZXCJRYD7.js +883 -0
- package/dist/chunk-ZXCJRYD7.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/ipfs/car.d.ts +59 -0
- package/dist/ipfs/car.d.ts.map +1 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
- package/dist/ipfs/ipfs-utils.d.ts +35 -0
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
- package/dist/ipfs.d.ts +4 -0
- package/dist/ipfs.d.ts.map +1 -0
- package/dist/ipfs.js +60 -0
- package/dist/ipfs.js.map +1 -0
- package/dist/ipns/ipns-record.d.ts +34 -0
- package/dist/ipns/ipns-record.d.ts.map +1 -0
- package/dist/ipns.d.ts +2 -0
- package/dist/ipns.d.ts.map +1 -0
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/connector.d.ts +9 -0
- package/dist/pubsub/connector.d.ts.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +14 -0
- package/dist/pubsub/pub-pull.d.ts.map +1 -0
- package/dist/pubsub/pubsub-types.d.ts +72 -0
- package/dist/pubsub/pubsub-types.d.ts.map +1 -0
- package/dist/pubsub/snap-push.d.ts +41 -0
- package/dist/pubsub/snap-push.d.ts.map +1 -0
- package/dist/pubsub/ucan-example.d.ts +3 -0
- package/dist/pubsub/ucan-example.d.ts.map +1 -0
- package/dist/pubsub/ucan.d.ts +16 -0
- package/dist/pubsub/ucan.d.ts.map +1 -0
- package/dist/pubsub.d.ts +5 -0
- package/dist/pubsub.d.ts.map +1 -0
- package/dist/pubsub.js +31 -0
- package/dist/pubsub.js.map +1 -0
- package/dist/query/basic.d.ts +105 -0
- package/dist/query/basic.d.ts.map +1 -0
- package/dist/query/divergences.d.ts +12 -0
- package/dist/query/divergences.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +4 -0
- package/dist/query/matchers.d.ts.map +1 -0
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/query-steps.d.ts +4 -0
- package/dist/query/query-steps.d.ts.map +1 -0
- package/dist/query/situations.d.ts +80 -0
- package/dist/query/situations.d.ts.map +1 -0
- package/dist/query/subscribable.d.ts +102 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +70 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +108 -0
- package/dist/query.js.map +1 -0
- package/dist/retrieve/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -0
- package/dist/retrieve/update-thread.d.ts +64 -0
- package/dist/retrieve/update-thread.d.ts.map +1 -0
- package/dist/retrieve.d.ts +2 -0
- package/dist/retrieve.d.ts.map +1 -0
- package/dist/retrieve.js +14 -0
- package/dist/retrieve.js.map +1 -0
- package/dist/thread/basic.d.ts +60 -0
- package/dist/thread/basic.d.ts.map +1 -0
- package/dist/thread/filters.d.ts +47 -0
- package/dist/thread/filters.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +31 -0
- package/dist/thread/mapped.d.ts.map +1 -0
- package/dist/thread/utils.d.ts +23 -0
- package/dist/thread/utils.d.ts.map +1 -0
- package/dist/thread/writeable.d.ts +41 -0
- package/dist/thread/writeable.d.ts.map +1 -0
- package/dist/thread.d.ts +6 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +54 -0
- package/dist/thread.js.map +1 -0
- package/dist/types/typescript-utils.d.ts +34 -0
- package/dist/types/typescript-utils.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/package.json +110 -0
- package/src/applog/applog-helpers.ts +150 -0
- package/src/applog/applog-utils.ts +398 -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/basic.ts +1061 -0
- package/src/query/divergences.ts +50 -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 +536 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +225 -0
- package/src/query/types.ts +155 -0
- package/src/query.ts +7 -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 +175 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +234 -0
- package/src/thread/mapped.ts +166 -0
- package/src/thread/utils.ts +146 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +5 -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
|
@@ -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,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
|
+
}
|