@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.
Files changed (180) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +3 -0
  3. package/dist/applog/applog-helpers.d.ts +47 -0
  4. package/dist/applog/applog-helpers.d.ts.map +1 -0
  5. package/dist/applog/applog-utils.d.ts +57 -0
  6. package/dist/applog/applog-utils.d.ts.map +1 -0
  7. package/dist/applog/datom-types.d.ts +128 -0
  8. package/dist/applog/datom-types.d.ts.map +1 -0
  9. package/dist/applog.d.ts +4 -0
  10. package/dist/applog.d.ts.map +1 -0
  11. package/dist/applog.js +101 -0
  12. package/dist/applog.js.map +1 -0
  13. package/dist/blockstore/index.d.ts +21 -0
  14. package/dist/blockstore/index.d.ts.map +1 -0
  15. package/dist/blockstore.d.ts +2 -0
  16. package/dist/blockstore.d.ts.map +1 -0
  17. package/dist/blockstore.js +24 -0
  18. package/dist/blockstore.js.map +1 -0
  19. package/dist/chunk-6MQKRL6W.js +86 -0
  20. package/dist/chunk-6MQKRL6W.js.map +1 -0
  21. package/dist/chunk-7MW34UEO.js +40 -0
  22. package/dist/chunk-7MW34UEO.js.map +1 -0
  23. package/dist/chunk-7Z5YDQKK.js +1 -0
  24. package/dist/chunk-7Z5YDQKK.js.map +1 -0
  25. package/dist/chunk-CY4NLISM.js +144 -0
  26. package/dist/chunk-CY4NLISM.js.map +1 -0
  27. package/dist/chunk-E46VTKTZ.js +1 -0
  28. package/dist/chunk-E46VTKTZ.js.map +1 -0
  29. package/dist/chunk-O43W7UW6.js +434 -0
  30. package/dist/chunk-O43W7UW6.js.map +1 -0
  31. package/dist/chunk-XIQSYEV3.js +1604 -0
  32. package/dist/chunk-XIQSYEV3.js.map +1 -0
  33. package/dist/chunk-XVGW4QC3.js +55 -0
  34. package/dist/chunk-XVGW4QC3.js.map +1 -0
  35. package/dist/chunk-YDAKBU6Q.js +9 -0
  36. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  37. package/dist/chunk-ZAADLBSB.js +36 -0
  38. package/dist/chunk-ZAADLBSB.js.map +1 -0
  39. package/dist/chunk-ZXCJRYD7.js +883 -0
  40. package/dist/chunk-ZXCJRYD7.js.map +1 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +354 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/ipfs/car.d.ts +59 -0
  46. package/dist/ipfs/car.d.ts.map +1 -0
  47. package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
  48. package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
  49. package/dist/ipfs/ipfs-utils.d.ts +35 -0
  50. package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
  51. package/dist/ipfs.d.ts +4 -0
  52. package/dist/ipfs.d.ts.map +1 -0
  53. package/dist/ipfs.js +60 -0
  54. package/dist/ipfs.js.map +1 -0
  55. package/dist/ipns/ipns-record.d.ts +34 -0
  56. package/dist/ipns/ipns-record.d.ts.map +1 -0
  57. package/dist/ipns.d.ts +2 -0
  58. package/dist/ipns.d.ts.map +1 -0
  59. package/dist/ipns.js +64 -0
  60. package/dist/ipns.js.map +1 -0
  61. package/dist/pubsub/connector.d.ts +9 -0
  62. package/dist/pubsub/connector.d.ts.map +1 -0
  63. package/dist/pubsub/pub-pull.d.ts +14 -0
  64. package/dist/pubsub/pub-pull.d.ts.map +1 -0
  65. package/dist/pubsub/pubsub-types.d.ts +72 -0
  66. package/dist/pubsub/pubsub-types.d.ts.map +1 -0
  67. package/dist/pubsub/snap-push.d.ts +41 -0
  68. package/dist/pubsub/snap-push.d.ts.map +1 -0
  69. package/dist/pubsub/ucan-example.d.ts +3 -0
  70. package/dist/pubsub/ucan-example.d.ts.map +1 -0
  71. package/dist/pubsub/ucan.d.ts +16 -0
  72. package/dist/pubsub/ucan.d.ts.map +1 -0
  73. package/dist/pubsub.d.ts +5 -0
  74. package/dist/pubsub.d.ts.map +1 -0
  75. package/dist/pubsub.js +31 -0
  76. package/dist/pubsub.js.map +1 -0
  77. package/dist/query/basic.d.ts +105 -0
  78. package/dist/query/basic.d.ts.map +1 -0
  79. package/dist/query/divergences.d.ts +12 -0
  80. package/dist/query/divergences.d.ts.map +1 -0
  81. package/dist/query/matchers.d.ts +4 -0
  82. package/dist/query/matchers.d.ts.map +1 -0
  83. package/dist/query/memoized.d.ts +66 -0
  84. package/dist/query/memoized.d.ts.map +1 -0
  85. package/dist/query/query-steps.d.ts +4 -0
  86. package/dist/query/query-steps.d.ts.map +1 -0
  87. package/dist/query/situations.d.ts +80 -0
  88. package/dist/query/situations.d.ts.map +1 -0
  89. package/dist/query/subscribable.d.ts +102 -0
  90. package/dist/query/subscribable.d.ts.map +1 -0
  91. package/dist/query/types.d.ts +70 -0
  92. package/dist/query/types.d.ts.map +1 -0
  93. package/dist/query.d.ts +8 -0
  94. package/dist/query.d.ts.map +1 -0
  95. package/dist/query.js +108 -0
  96. package/dist/query.js.map +1 -0
  97. package/dist/retrieve/index.d.ts +2 -0
  98. package/dist/retrieve/index.d.ts.map +1 -0
  99. package/dist/retrieve/update-thread.d.ts +64 -0
  100. package/dist/retrieve/update-thread.d.ts.map +1 -0
  101. package/dist/retrieve.d.ts +2 -0
  102. package/dist/retrieve.d.ts.map +1 -0
  103. package/dist/retrieve.js +14 -0
  104. package/dist/retrieve.js.map +1 -0
  105. package/dist/thread/basic.d.ts +60 -0
  106. package/dist/thread/basic.d.ts.map +1 -0
  107. package/dist/thread/filters.d.ts +47 -0
  108. package/dist/thread/filters.d.ts.map +1 -0
  109. package/dist/thread/mapped.d.ts +31 -0
  110. package/dist/thread/mapped.d.ts.map +1 -0
  111. package/dist/thread/utils.d.ts +23 -0
  112. package/dist/thread/utils.d.ts.map +1 -0
  113. package/dist/thread/writeable.d.ts +41 -0
  114. package/dist/thread/writeable.d.ts.map +1 -0
  115. package/dist/thread.d.ts +6 -0
  116. package/dist/thread.d.ts.map +1 -0
  117. package/dist/thread.js +54 -0
  118. package/dist/thread.js.map +1 -0
  119. package/dist/types/typescript-utils.d.ts +34 -0
  120. package/dist/types/typescript-utils.d.ts.map +1 -0
  121. package/dist/types.d.ts +2 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +26 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/utils/debug-name.d.ts +13 -0
  126. package/dist/utils/debug-name.d.ts.map +1 -0
  127. package/dist/utils.d.ts +4 -0
  128. package/dist/utils.d.ts.map +1 -0
  129. package/dist/utils.js +9 -0
  130. package/dist/utils.js.map +1 -0
  131. package/package.json +110 -0
  132. package/src/applog/applog-helpers.ts +150 -0
  133. package/src/applog/applog-utils.ts +398 -0
  134. package/src/applog/datom-types.ts +148 -0
  135. package/src/applog.ts +3 -0
  136. package/src/blockstore/index.ts +36 -0
  137. package/src/blockstore.ts +1 -0
  138. package/src/index.ts +8 -0
  139. package/src/ipfs/car.ts +291 -0
  140. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  141. package/src/ipfs/ipfs-utils.ts +132 -0
  142. package/src/ipfs.ts +3 -0
  143. package/src/ipns/ipns-record.ts +115 -0
  144. package/src/ipns.ts +1 -0
  145. package/src/pubsub/UCAN Specs Overview.md +217 -0
  146. package/src/pubsub/connector.ts +9 -0
  147. package/src/pubsub/pub-pull.ts +31 -0
  148. package/src/pubsub/pubsub-types.ts +90 -0
  149. package/src/pubsub/snap-push.ts +277 -0
  150. package/src/pubsub/ucan-example.ts +61 -0
  151. package/src/pubsub/ucan.ts +56 -0
  152. package/src/pubsub.ts +4 -0
  153. package/src/query/basic.ts +1061 -0
  154. package/src/query/divergences.ts +50 -0
  155. package/src/query/matchers.ts +8 -0
  156. package/src/query/memoized.test.ts +151 -0
  157. package/src/query/memoized.ts +180 -0
  158. package/src/query/query-steps.ts +4 -0
  159. package/src/query/query.test.ts +536 -0
  160. package/src/query/situations.ts +261 -0
  161. package/src/query/subscribable.test.ts +245 -0
  162. package/src/query/subscribable.ts +225 -0
  163. package/src/query/types.ts +155 -0
  164. package/src/query.ts +7 -0
  165. package/src/retrieve/index.ts +1 -0
  166. package/src/retrieve/update-thread.ts +248 -0
  167. package/src/retrieve.ts +1 -0
  168. package/src/test/perf/query.1m.perf.test.ts +94 -0
  169. package/src/test/perf/query.perf.test.ts +389 -0
  170. package/src/test/perf/query.realdata.perf.test.ts +175 -0
  171. package/src/thread/basic.ts +209 -0
  172. package/src/thread/filters.ts +234 -0
  173. package/src/thread/mapped.ts +166 -0
  174. package/src/thread/utils.ts +146 -0
  175. package/src/thread/writeable.ts +163 -0
  176. package/src/thread.ts +5 -0
  177. package/src/types/typescript-utils.ts +64 -0
  178. package/src/types.ts +1 -0
  179. package/src/utils/debug-name.ts +54 -0
  180. 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,8 @@
1
+ import { DatomPart } from '../applog/datom-types.ts'
2
+
3
+ export function includes(str: string) {
4
+ return (vl: DatomPart) => vl?.includes?.(str)
5
+ }
6
+ export function includedIn(arr: string[]) {
7
+ return (vl: DatomPart) => arr?.includes?.(vl)
8
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export const q = {
2
+ not: () => {
3
+ },
4
+ }