@wovin/core 0.3.4 → 0.3.6

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 (34) hide show
  1. package/dist/applog.js +1 -1
  2. package/dist/{chunk-ERVSWD63.js → chunk-AFRRFDNL.js} +2 -2
  3. package/dist/{chunk-XEUQDEA4.js → chunk-B22AIVJH.js} +2 -2
  4. package/dist/{chunk-OB7M55WA.js → chunk-EZZVLYYC.js} +3 -3
  5. package/dist/{chunk-PTZJ7LVV.js → chunk-IC6WD7RV.js} +3 -3
  6. package/dist/{chunk-7VHIXHNF.js → chunk-OUQE6LG4.js} +42 -3
  7. package/dist/chunk-OUQE6LG4.js.map +1 -0
  8. package/dist/{chunk-GQDLPZEZ.js → chunk-PNNJMXZI.js} +2 -2
  9. package/dist/{chunk-523IW54O.js → chunk-PSIMJH6F.js} +3 -3
  10. package/dist/index.js +7 -7
  11. package/dist/ipfs.js +4 -4
  12. package/dist/pubsub.js +4 -4
  13. package/dist/query.js +3 -3
  14. package/dist/retrieve.js +4 -4
  15. package/dist/thread/basic.d.ts +18 -1
  16. package/dist/thread/basic.d.ts.map +1 -1
  17. package/dist/thread/mapped.d.ts +3 -1
  18. package/dist/thread/mapped.d.ts.map +1 -1
  19. package/dist/thread/writeable.d.ts.map +1 -1
  20. package/dist/thread.js +1 -1
  21. package/dist/viewmodel/index.js +2 -2
  22. package/package.json +1 -1
  23. package/src/thread/basic.ts +31 -3
  24. package/src/thread/insertMissing-perf.test.ts +95 -0
  25. package/src/thread/mapped.ts +13 -1
  26. package/src/thread/membership-cache.test.ts +133 -0
  27. package/src/thread/writeable.ts +2 -1
  28. package/dist/chunk-7VHIXHNF.js.map +0 -1
  29. /package/dist/{chunk-ERVSWD63.js.map → chunk-AFRRFDNL.js.map} +0 -0
  30. /package/dist/{chunk-XEUQDEA4.js.map → chunk-B22AIVJH.js.map} +0 -0
  31. /package/dist/{chunk-OB7M55WA.js.map → chunk-EZZVLYYC.js.map} +0 -0
  32. /package/dist/{chunk-PTZJ7LVV.js.map → chunk-IC6WD7RV.js.map} +0 -0
  33. /package/dist/{chunk-GQDLPZEZ.js.map → chunk-PNNJMXZI.js.map} +0 -0
  34. /package/dist/{chunk-523IW54O.js.map → chunk-PSIMJH6F.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@ import "./chunk-E46VTKTZ.js";
2
2
  import "./chunk-7Z5YDQKK.js";
3
3
  import {
4
4
  fetchSnapshotChainUntil
5
- } from "./chunk-XEUQDEA4.js";
5
+ } from "./chunk-B22AIVJH.js";
6
6
  import {
7
7
  agentToShortHash,
8
8
  integratePub,
9
9
  isShare,
10
10
  isSubscription
11
- } from "./chunk-ERVSWD63.js";
11
+ } from "./chunk-AFRRFDNL.js";
12
12
  import {
13
13
  createEpochSnapshot,
14
14
  liveEntityCollection,
@@ -16,11 +16,11 @@ import {
16
16
  queryDivergencesByPrev,
17
17
  resolveTimeSpec,
18
18
  serializeEpochSnapshot
19
- } from "./chunk-523IW54O.js";
19
+ } from "./chunk-PSIMJH6F.js";
20
20
  import {
21
21
  updateThreadFromSnapshot,
22
22
  withBlockCache
23
- } from "./chunk-OB7M55WA.js";
23
+ } from "./chunk-EZZVLYYC.js";
24
24
  import {
25
25
  carFromBlob,
26
26
  chunkApplogs,
@@ -37,7 +37,7 @@ import {
37
37
  prepareSnapshotForPush,
38
38
  streamReaderToIterable,
39
39
  unchunkApplogsBlock
40
- } from "./chunk-PTZJ7LVV.js";
40
+ } from "./chunk-IC6WD7RV.js";
41
41
  import "./chunk-YDAKBU6Q.js";
42
42
  import {
43
43
  LiveQueryResult,
@@ -81,7 +81,7 @@ import {
81
81
  throwOnTimeout,
82
82
  withTimeout,
83
83
  withoutDeleted
84
- } from "./chunk-GQDLPZEZ.js";
84
+ } from "./chunk-PNNJMXZI.js";
85
85
  import {
86
86
  AppLogNoCidTB,
87
87
  AppLogNoCidTBC,
@@ -189,7 +189,7 @@ import {
189
189
  withAg,
190
190
  withPvFrom,
191
191
  withTs
192
- } from "./chunk-7VHIXHNF.js";
192
+ } from "./chunk-OUQE6LG4.js";
193
193
  import {
194
194
  BOOL,
195
195
  Bool,
package/dist/ipfs.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  fetchSnapshotChainUntil
3
- } from "./chunk-XEUQDEA4.js";
3
+ } from "./chunk-B22AIVJH.js";
4
4
  import {
5
5
  carFromBlob,
6
6
  collectDagBlocks,
@@ -11,9 +11,9 @@ import {
11
11
  makeCarBlob,
12
12
  makeCarOut,
13
13
  streamReaderToIterable
14
- } from "./chunk-PTZJ7LVV.js";
14
+ } from "./chunk-IC6WD7RV.js";
15
15
  import "./chunk-YDAKBU6Q.js";
16
- import "./chunk-GQDLPZEZ.js";
16
+ import "./chunk-PNNJMXZI.js";
17
17
  import {
18
18
  MULTICODEC_IPNS_KEY,
19
19
  areCidsEqual,
@@ -29,7 +29,7 @@ import {
29
29
  prepareForPub,
30
30
  toIpnsString,
31
31
  tryParseCID
32
- } from "./chunk-7VHIXHNF.js";
32
+ } from "./chunk-OUQE6LG4.js";
33
33
  import "./chunk-ZAADLBSB.js";
34
34
  export {
35
35
  MULTICODEC_IPNS_KEY,
package/dist/pubsub.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  integratePub,
4
4
  isShare,
5
5
  isSubscription
6
- } from "./chunk-ERVSWD63.js";
6
+ } from "./chunk-AFRRFDNL.js";
7
7
  import {
8
8
  chunkApplogs,
9
9
  encodeSnapshotApplogsAsCar,
@@ -11,10 +11,10 @@ import {
11
11
  isSnapBlockChunks,
12
12
  prepareSnapshotForPush,
13
13
  unchunkApplogsBlock
14
- } from "./chunk-PTZJ7LVV.js";
14
+ } from "./chunk-IC6WD7RV.js";
15
15
  import "./chunk-YDAKBU6Q.js";
16
- import "./chunk-GQDLPZEZ.js";
17
- import "./chunk-7VHIXHNF.js";
16
+ import "./chunk-PNNJMXZI.js";
17
+ import "./chunk-OUQE6LG4.js";
18
18
  import "./chunk-ZAADLBSB.js";
19
19
  export {
20
20
  agentToShortHash,
package/dist/query.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  queryDivergencesByPrev,
6
6
  resolveTimeSpec,
7
7
  serializeEpochSnapshot
8
- } from "./chunk-523IW54O.js";
8
+ } from "./chunk-PSIMJH6F.js";
9
9
  import {
10
10
  LiveQueryResult,
11
11
  QueryNode,
@@ -48,7 +48,7 @@ import {
48
48
  throwOnTimeout,
49
49
  withTimeout,
50
50
  withoutDeleted
51
- } from "./chunk-GQDLPZEZ.js";
51
+ } from "./chunk-PNNJMXZI.js";
52
52
  import {
53
53
  SubscribableArrayImpl,
54
54
  SubscribableImpl,
@@ -61,7 +61,7 @@ import {
61
61
  memoizedFn,
62
62
  prettifyThreadName,
63
63
  refCountedMemoizedFn
64
- } from "./chunk-7VHIXHNF.js";
64
+ } from "./chunk-OUQE6LG4.js";
65
65
  import "./chunk-ZAADLBSB.js";
66
66
  export {
67
67
  LiveQueryResult,
package/dist/retrieve.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  updateThreadFromSnapshot,
3
3
  withBlockCache
4
- } from "./chunk-OB7M55WA.js";
5
- import "./chunk-PTZJ7LVV.js";
4
+ } from "./chunk-EZZVLYYC.js";
5
+ import "./chunk-IC6WD7RV.js";
6
6
  import "./chunk-YDAKBU6Q.js";
7
- import "./chunk-GQDLPZEZ.js";
8
- import "./chunk-7VHIXHNF.js";
7
+ import "./chunk-PNNJMXZI.js";
8
+ import "./chunk-OUQE6LG4.js";
9
9
  import "./chunk-ZAADLBSB.js";
10
10
  export {
11
11
  updateThreadFromSnapshot,
@@ -16,11 +16,28 @@ export declare abstract class Thread implements SubscribableArray<Applog> {
16
16
  protected _subscribers: ((event: ThreadEvent) => void)[];
17
17
  /** Monotonic counter incremented on every mutation. Used by memoizedFn to invalidate caches. */
18
18
  _version: number;
19
+ /**
20
+ * Memoized cid→membership lookups, rebuilt lazily on first read after a mutation.
21
+ * Previously these getters rebuilt a fresh Set/Map on EVERY access, which turned a
22
+ * bulk `insertMissing(N)` (N × hasApplog over a thread of M) into O(N·M) — pegging a
23
+ * core for tens of minutes on ~90k-applog threads. Now the collection is built once
24
+ * per mutation batch and reused across the whole filter loop. Invalidated in
25
+ * {@link invalidateApplogCaches}, called from every `_applogs` mutation path.
26
+ */
27
+ protected _cidSet: Set<CidString> | null;
28
+ protected _byCid: Map<CidString, Applog> | null;
19
29
  constructor(name: string, /* = null */ parents: ArrayOrSingle<Thread> | readonly Thread[] | null, filters: readonly string[], _applogs?: Applog[]);
20
30
  get readOnly(): any;
21
31
  insert(appLogsToInsert: ArrayOrSingle<ApplogForInsert>): any;
22
32
  insertRaw(appLogsToInsert: readonly Applog[]): any;
23
33
  subscribe(callback: (event: ThreadEvent) => void, type?: 'derived' | 'reaction'): () => void;
34
+ /**
35
+ * Drop memoized cid lookups. MUST be called from every path that mutates `_applogs`
36
+ * (or reassigns it). {@link notifySubscribers} covers the notify-emitting mutations
37
+ * (insertRaw, MappedThread remap/parent-update); non-notifying mutations (e.g.
38
+ * {@link WriteableThread.purge}) must call this explicitly.
39
+ */
40
+ protected invalidateApplogCaches(): void;
24
41
  protected notifySubscribers(event: ThreadEvent): void;
25
42
  get items(): readonly Applog[];
26
43
  dispose(): void;
@@ -40,7 +57,7 @@ export declare abstract class Thread implements SubscribableArray<Applog> {
40
57
  get latestLog(): Applog;
41
58
  hasApplog(applog: Applog, byRef: boolean): boolean;
42
59
  hasApplogCid(cid: CID | CidString): boolean;
43
- get applogsByCid(): Map<CidString, Applog>;
60
+ get applogsByCid(): ReadonlyMap<CidString, Applog>;
44
61
  getApplog(cid: CID | CidString): Applog;
45
62
  hasApplogWithDiffTs(applog: ApplogForInsert): Applog;
46
63
  get isEmpty(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"basic.d.ts","sourceRoot":"","sources":["../../src/thread/basic.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"basic.d.ts","sourceRoot":"","sources":["../../src/thread/basic.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAGlC,OAAO,EAAE,KAAK,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAClF,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAG3D,OAAO,EAAiB,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAI3E,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;AAC5C,8DAA8D;AAC9D,eAAO,MAAM,WAAW,yBAAmB,CAAA;AAE3C,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;AAIxD,8BAAsB,MAAO,YAAW,iBAAiB,CAAC,MAAM,CAAC;IAmB/D,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE;IArB7B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;IACrD,SAAS,CAAC,mBAAmB,EAAE,CAAC,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC,EAAE,CAAK;IACpE,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC,EAAE,CAAK;IAC7D,gGAAgG;IAChG,QAAQ,SAAI;IACZ;;;;;;;OAOG;IACH,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,GAAG,IAAI,CAAO;IAC/C,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,IAAI,CAAO;gBAG5C,IAAI,EAAE,MAAM,EAAE,YAAY,CACnC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,EACzD,OAAO,EAAE,SAAS,MAAM,EAAE,EAChB,QAAQ,GAAE,MAAM,EAAO;IASlC,IAAI,QAAQ,QAGX;IAEM,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,eAAe,CAAC;IAMtD,SAAS,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE;IAOnD,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU;IAS/E;;;;;OAKG;IACH,SAAS,CAAC,sBAAsB;IAKhC,SAAS,CAAC,iBAAiB,CAAC,KAAK,EAAE,WAAW;IAkB9C,IAAI,KAAK,IAAI,SAAS,MAAM,EAAE,CAE7B;IAED,OAAO;IAKP,IAAI,OAAO,IAAI,SAAS,MAAM,EAAE,CAG/B;IACD,IAAI,WAAW,IAAI,SAAS,SAAS,EAAE,CAEtC;IACD,IAAI,aAAa,IAAI,WAAW,CAAC,SAAS,CAAC,CAK1C;IAEM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,CAAC;IAIvC,IAAW,QAAQ;;;MAElB;IACD,IAAW,SAAS;;;MAEnB;IAED,IAAI,QAAQ,WAEX;IACD,IAAI,SAAS,WAEZ;IACM,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAgBxC,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS;IAGxC,IAAI,YAAY,IAAI,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,CAKjD;IACM,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS;IAO9B,mBAAmB,CAAC,MAAM,EAAE,eAAe;IAkBlD,IAAI,OAAO,YAEV;IACD,IAAI,IAAI,WAEP;IACD,IAAI,MAAM,WAET;IACD,IAAI,aAAa,WAEhB;IACD,IAAI,oBAAoB,WAEvB;IACD,IAAI,UAAU,WAEb;IACD,IAAI,UAAU,YAEb;CAeD;AAED,eAAO,MAAM,iBAAiB,GAAI,cAAc,eAAe,sBAAyE,CAAA;AAExI,qBAAa,YAAa,SAAQ,MAAM;IACvC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;gBAIhD,IAAI,EAAE,MAAM,EAAE,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,EACzD,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,QAAQ,EAAE,MAAM,EAAE;IAKnB,IAAI,QAAQ,YAEX;CACD"}
@@ -1,4 +1,4 @@
1
- import { Applog, ApplogForInsert } from '../applog/datom-types.ts';
1
+ import { Applog, ApplogForInsert, CidString } from '../applog/datom-types.ts';
2
2
  import { Thread, ThreadEvent } from './basic.ts';
3
3
  /** Delta event — only incremental changes (no init) */
4
4
  export type DeltaEvent = {
@@ -52,5 +52,7 @@ export declare class MappedThread extends Thread {
52
52
  triggerRemap(): void;
53
53
  protected onParentUpdate(sourceThread: Thread, event: ThreadEvent): void;
54
54
  get readOnly(): boolean;
55
+ get applogsCidSet(): ReadonlySet<CidString>;
56
+ get applogsByCid(): ReadonlyMap<CidString, Applog>;
55
57
  }
56
58
  //# sourceMappingURL=mapped.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapped.d.ts","sourceRoot":"","sources":["../../src/thread/mapped.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAGlE,OAAO,EAAe,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAY7D,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;CAAE,CAAA;AAExF,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC5B,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,gGAAgG;IAChG,OAAO,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;IAE7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,KAAK,UAAU,GAAG,IAAI,CAAA;CACtE;AAKD,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,eAAe,EAAE,KAAK,MAAM,EAAE,GAAG,eAAe,EAAE,GAAG,IAAI,CAAA;AAIlI,qBAAa,YAAa,SAAQ,MAAM;IAwBtC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,SAAS,CAAC;IA5BnB,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB;IASxE,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM;IAWhC,OAAO,CAAC,oBAAoB,CAAkC;gBAGpD,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACnC,OAAO,EAAE,SAAS,MAAM,EAAE,EAClB,WAAW,EAAE,gBAAgB,GAAG,IAAI,EACpC,YAAY,GAAE,iBAAwB,EACtC,SAAS,CAAC,EAAE,OAAO;IAwBrB,MAAM,CAAC,eAAe,EAAE,eAAe,EAAE;IAMzC,SAAS,CAAC,eAAe,EAAE,MAAM,EAAE;IAO1C,OAAO,CAAC,kBAAkB;IAe1B,8DAA8D;IAC9D,OAAO;IASP,2FAA2F;IAC3F,UAAU,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE;IAOxC,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU;IAO/E,sDAAsD;IACtD,YAAY;IASZ,SAAS,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW;IA2DjE,IAAI,QAAQ,YAEX;CACD"}
1
+ {"version":3,"file":"mapped.d.ts","sourceRoot":"","sources":["../../src/thread/mapped.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAG7E,OAAO,EAAe,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAY7D,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAA;CAAE,CAAA;AAExF,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC5B,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,8DAA8D;IAC9D,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,gGAAgG;IAChG,OAAO,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;IAE7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,KAAK,UAAU,GAAG,IAAI,CAAA;CACtE;AAKD,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,eAAe,EAAE,KAAK,MAAM,EAAE,GAAG,eAAe,EAAE,GAAG,IAAI,CAAA;AAIlI,qBAAa,YAAa,SAAQ,MAAM;IAwBtC,QAAQ,CAAC,IAAI,EAAE,MAAM;IAGrB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,SAAS,CAAC;IA5BnB,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB;IASxE,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM;IAWhC,OAAO,CAAC,oBAAoB,CAAkC;gBAGpD,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACnC,OAAO,EAAE,SAAS,MAAM,EAAE,EAClB,WAAW,EAAE,gBAAgB,GAAG,IAAI,EACpC,YAAY,GAAE,iBAAwB,EACtC,SAAS,CAAC,EAAE,OAAO;IAwBrB,MAAM,CAAC,eAAe,EAAE,eAAe,EAAE;IAMzC,SAAS,CAAC,eAAe,EAAE,MAAM,EAAE;IAO1C,OAAO,CAAC,kBAAkB;IAe1B,8DAA8D;IAC9D,OAAO;IASP,2FAA2F;IAC3F,UAAU,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE;IAOxC,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU;IAO/E,sDAAsD;IACtD,YAAY;IASZ,SAAS,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW;IA2DjE,IAAI,QAAQ,YAEX;IAOD,IAAI,aAAa,IAAI,WAAW,CAAC,SAAS,CAAC,CAE1C;IACD,IAAI,YAAY,IAAI,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,CAEjD;CACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"writeable.d.ts","sourceRoot":"","sources":["../../src/thread/writeable.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,eAAe,EAAE,SAAS,EAAiB,MAAM,0BAA0B,CAAA;AACjG,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAInC,8BAAsB,eAAgB,SAAQ,MAAM;gBAElD,IAAI,EAAE,MAAM,EAEZ,OAAO,EAAE,MAAM,EAAO,EACtB,OAAO,EAAE,SAAS,MAAM,EAAE;IAKpB,KAAK,CAAC,WAAW,EAAE,SAAS,EAAE;IAM9B,MAAM,CAAC,eAAe,EAAE,eAAe,EAAE;IAWhD;;;;OAIG;IACI,aAAa,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,UAAQ,GAAG,MAAM,EAAE;IASjF;;;;;;;;;;;;;;OAcG;IACI,SAAS,CAAC,eAAe,EAAE,MAAM,EAAE;IA6D1C,IAAI,QAAQ,YAEX;IAED,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CACzD;AACD,qBAAa,cAAe,SAAQ,eAAe;IAgBjD,QAAQ,CAAC,SAAS,EAAE,OAAO;IAf5B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM;IAG1B,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ;IAGnE,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;gBAMjE,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,EACjB,SAAS,EAAE,OAAO;IAK5B,IAAI,QAAQ,YAEX;cAEe,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE;CAMtC"}
1
+ {"version":3,"file":"writeable.d.ts","sourceRoot":"","sources":["../../src/thread/writeable.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,eAAe,EAAE,SAAS,EAAiB,MAAM,0BAA0B,CAAA;AACjG,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAInC,8BAAsB,eAAgB,SAAQ,MAAM;gBAElD,IAAI,EAAE,MAAM,EAEZ,OAAO,EAAE,MAAM,EAAO,EACtB,OAAO,EAAE,SAAS,MAAM,EAAE;IAKpB,KAAK,CAAC,WAAW,EAAE,SAAS,EAAE;IAO9B,MAAM,CAAC,eAAe,EAAE,eAAe,EAAE;IAWhD;;;;OAIG;IACI,aAAa,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,UAAQ,GAAG,MAAM,EAAE;IASjF;;;;;;;;;;;;;;OAcG;IACI,SAAS,CAAC,eAAe,EAAE,MAAM,EAAE;IA6D1C,IAAI,QAAQ,YAEX;IAED,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CACzD;AACD,qBAAa,cAAe,SAAQ,eAAe;IAgBjD,QAAQ,CAAC,SAAS,EAAE,OAAO;IAf5B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM;IAG1B,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ;IAGnE,MAAM,CAAC,iBAAiB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;gBAMjE,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,EACjB,SAAS,EAAE,OAAO;IAK5B,IAAI,QAAQ,YAEX;cAEe,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE;CAMtC"}
package/dist/thread.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  rollingFilter,
27
27
  rollingMapper,
28
28
  simpleApplogMapper
29
- } from "./chunk-7VHIXHNF.js";
29
+ } from "./chunk-OUQE6LG4.js";
30
30
  import "./chunk-ZAADLBSB.js";
31
31
  export {
32
32
  MappedThread,
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  lastWriteWins,
15
15
  liveEntityAt
16
- } from "../chunk-GQDLPZEZ.js";
16
+ } from "../chunk-PNNJMXZI.js";
17
17
  import {
18
18
  EntityID_LENGTH,
19
19
  SubscribableImpl,
@@ -22,7 +22,7 @@ import {
22
22
  isInitEvent,
23
23
  mintNewEntity,
24
24
  rollingFilter
25
- } from "../chunk-7VHIXHNF.js";
25
+ } from "../chunk-OUQE6LG4.js";
26
26
  import "../chunk-ZAADLBSB.js";
27
27
 
28
28
  // src/viewmodel/builder.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wovin/core",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -28,6 +28,16 @@ export abstract class Thread implements SubscribableArray<Applog> {
28
28
  protected _subscribers: ((event: ThreadEvent) => void)[] = []
29
29
  /** Monotonic counter incremented on every mutation. Used by memoizedFn to invalidate caches. */
30
30
  _version = 0
31
+ /**
32
+ * Memoized cid→membership lookups, rebuilt lazily on first read after a mutation.
33
+ * Previously these getters rebuilt a fresh Set/Map on EVERY access, which turned a
34
+ * bulk `insertMissing(N)` (N × hasApplog over a thread of M) into O(N·M) — pegging a
35
+ * core for tens of minutes on ~90k-applog threads. Now the collection is built once
36
+ * per mutation batch and reused across the whole filter loop. Invalidated in
37
+ * {@link invalidateApplogCaches}, called from every `_applogs` mutation path.
38
+ */
39
+ protected _cidSet: Set<CidString> | null = null
40
+ protected _byCid: Map<CidString, Applog> | null = null
31
41
 
32
42
  constructor(
33
43
  readonly name: string, /* = null */
@@ -69,8 +79,20 @@ export abstract class Thread implements SubscribableArray<Applog> {
69
79
  }
70
80
  }
71
81
 
82
+ /**
83
+ * Drop memoized cid lookups. MUST be called from every path that mutates `_applogs`
84
+ * (or reassigns it). {@link notifySubscribers} covers the notify-emitting mutations
85
+ * (insertRaw, MappedThread remap/parent-update); non-notifying mutations (e.g.
86
+ * {@link WriteableThread.purge}) must call this explicitly.
87
+ */
88
+ protected invalidateApplogCaches() {
89
+ this._cidSet = null
90
+ this._byCid = null
91
+ }
92
+
72
93
  protected notifySubscribers(event: ThreadEvent) {
73
94
  this._version++
95
+ this.invalidateApplogCaches()
74
96
  DEBUG(`[thread: ${this.name}] notifying`, this._derivedSubscribers.length, 'derived +', this._subscribers.length, 'subscribers of', {
75
97
  ...event,
76
98
  subs: this._subscribers,
@@ -103,7 +125,10 @@ export abstract class Thread implements SubscribableArray<Applog> {
103
125
  return this._applogs.map(l => l.cid)
104
126
  }
105
127
  get applogsCidSet(): ReadonlySet<CidString> {
106
- return new Set(this._applogs.map(l => l.cid))
128
+ if (this._cidSet === null) {
129
+ this._cidSet = new Set(this._applogs.map(l => l.cid))
130
+ }
131
+ return this._cidSet
107
132
  }
108
133
 
109
134
  public map<R>(fn: (applog: Applog) => R) {
@@ -142,8 +167,11 @@ export abstract class Thread implements SubscribableArray<Applog> {
142
167
  public hasApplogCid(cid: CID | CidString) {
143
168
  return this.applogsCidSet.has(cid.toString()) // O(1) via Set vs O(n) via includes
144
169
  }
145
- get applogsByCid() {
146
- return new Map(this.applogs.map(log => [log.cid, log]))
170
+ get applogsByCid(): ReadonlyMap<CidString, Applog> {
171
+ if (this._byCid === null) {
172
+ this._byCid = new Map(this._applogs.map(log => [log.cid, log]))
173
+ }
174
+ return this._byCid
147
175
  }
148
176
  public getApplog(cid: CID | CidString) {
149
177
  return this.applogsByCid.get(cid.toString())
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Perf regression guard for the O(n²) hang in Thread membership checks.
3
+ *
4
+ * Before: `applogsCidSet` / `applogsByCid` rebuilt a fresh Set/Map on EVERY access.
5
+ * `insertMissing(N)` runs `hasApplog` (→ applogsCidSet) N times over a thread of M,
6
+ * and `insertRaw` filters existing again — so merging N applogs into a thread of M
7
+ * cost O(N·M) Set rebuilds. A daemon reloading ~89k local applogs into a thread that
8
+ * already held ~89k pegged a core at 98% for 45–70 min (~8 billion ops).
9
+ *
10
+ * After: the cid Set/Map is memoized and invalidated only on mutation, so the whole
11
+ * merge is O(N + M). This test inserts 50k applogs into a 50k thread and asserts it
12
+ * finishes in well under a second (was minutes).
13
+ */
14
+ import { describe, expect, it } from 'vitest'
15
+ import { finalizeApplogForInsert } from '../applog/applog-helpers.ts'
16
+ import { sortApplogsByTs } from '../applog/applog-utils.ts'
17
+ import type { Applog, ApplogForInsert } from '../applog/datom-types.ts'
18
+ import { ThreadInMemory } from './writeable.ts'
19
+
20
+ /** Build N valid, unique applogs (real CIDs) as cheaply as we can. */
21
+ function makeLogs(count: number, startId = 0): Applog[] {
22
+ const logs: Applog[] = new Array(count)
23
+ for (let i = 0; i < count; i++) {
24
+ const id = startId + i
25
+ logs[i] = finalizeApplogForInsert(
26
+ {
27
+ en: `entity-${id}`,
28
+ at: 'value',
29
+ vl: id,
30
+ ts: new Date(1700000000000 + id * 1000).toISOString(),
31
+ pv: null,
32
+ ag: 'perfAgent',
33
+ } as ApplogForInsert,
34
+ {},
35
+ )
36
+ }
37
+ return sortApplogsByTs(logs)
38
+ }
39
+
40
+ describe('insertMissing perf (O(n²) regression guard)', () => {
41
+ it('merges 50k applogs into a 50k thread in well under 1s', () => {
42
+ const N = 50_000
43
+ const logs = makeLogs(N)
44
+
45
+ // Thread already holds all N — the daemon's "reload local applogs already present" case.
46
+ const thread = ThreadInMemory.fromArray([...logs], 'perf-thread')
47
+ expect(thread.size).toBe(N)
48
+
49
+ const t0 = performance.now()
50
+ const inserted = thread.insertMissing(logs) // every log already present → nothing to insert
51
+ const ms = performance.now() - t0
52
+
53
+ expect(inserted).toEqual([]) // all filtered out as existing
54
+ expect(thread.size).toBe(N)
55
+ // eslint-disable-next-line no-console
56
+ console.log(`[insertMissing-perf] insertMissing(${N}) into ${N}-thread: ${ms.toFixed(1)}ms`)
57
+ expect(ms).toBeLessThan(1000)
58
+ })
59
+
60
+ it('merges 25k new + 25k existing correctly and fast', () => {
61
+ const existing = makeLogs(25_000, 0)
62
+ const fresh = makeLogs(25_000, 1_000_000)
63
+ const thread = ThreadInMemory.fromArray([...existing], 'perf-thread-mixed')
64
+
65
+ const mixed = sortApplogsByTs([...existing, ...fresh])
66
+ const t0 = performance.now()
67
+ const inserted = thread.insertMissing(mixed)
68
+ const ms = performance.now() - t0
69
+
70
+ expect(inserted).toHaveLength(25_000) // only the fresh ones
71
+ expect(thread.size).toBe(50_000)
72
+ // Cache stayed correct across the mutation: freshly-inserted cids are now members.
73
+ expect(thread.hasApplogCid(fresh[0].cid)).toBe(true)
74
+ expect(thread.hasApplogCid(fresh[fresh.length - 1].cid)).toBe(true)
75
+ // eslint-disable-next-line no-console
76
+ console.log(`[insertMissing-perf] insertMissing(25k new + 25k dup): ${ms.toFixed(1)}ms`)
77
+ expect(ms).toBeLessThan(1000)
78
+ })
79
+
80
+ it('membership cache invalidates on insert and purge', () => {
81
+ const logs = makeLogs(1_000)
82
+ const thread = ThreadInMemory.fromArray([...logs.slice(0, 500)], 'cache-correctness')
83
+
84
+ // Not present yet → build cache, miss.
85
+ expect(thread.hasApplogCid(logs[600].cid)).toBe(false)
86
+ // Insert → cache must invalidate so the new cid is visible.
87
+ thread.insertRaw([logs[600]])
88
+ expect(thread.hasApplogCid(logs[600].cid)).toBe(true)
89
+ expect(thread.getApplog(logs[600].cid)?.en).toBe(logs[600].en)
90
+ // Purge → cache must invalidate so the removed cid disappears.
91
+ thread.purge([logs[600].cid])
92
+ expect(thread.hasApplogCid(logs[600].cid)).toBe(false)
93
+ expect(thread.getApplog(logs[600].cid)).toBeUndefined()
94
+ })
95
+ })
@@ -1,6 +1,6 @@
1
1
  import { Logger } from 'besonders-logger'
2
2
  import { DebouncedFunc, pull, sortedIndexBy } from 'lodash-es'
3
- import { Applog, ApplogForInsert } from '../applog/datom-types.ts'
3
+ import { Applog, ApplogForInsert, CidString } from '../applog/datom-types.ts'
4
4
  import { applogsSummary } from '../applog/applog-utils.ts'
5
5
  import { arrayIfSingle } from '../types/typescript-utils.ts'
6
6
  import { isInitEvent, Thread, ThreadEvent } from './basic.ts'
@@ -231,4 +231,16 @@ export class MappedThread extends Thread {
231
231
  get readOnly() {
232
232
  return this._readOnly ?? false
233
233
  }
234
+
235
+ // A no-derivation MappedThread SHARES its parent's `_applogs` array (see constructor) and never
236
+ // mutates it itself — the parent does, and the parent invalidates only its own cid caches. So a
237
+ // local cache here would go stale after a parent insert. Delegate membership lookups to the parent
238
+ // (chaining up to the array's real owner), whose cache is always fresh. Derived threads own their
239
+ // array and use the inherited, self-invalidating cache.
240
+ get applogsCidSet(): ReadonlySet<CidString> {
241
+ return this._derivation ? super.applogsCidSet : this.parents[0].applogsCidSet
242
+ }
243
+ get applogsByCid(): ReadonlyMap<CidString, Applog> {
244
+ return this._derivation ? super.applogsByCid : this.parents[0].applogsByCid
245
+ }
234
246
  }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Guards for the memoized cid-membership caches (`_cidSet` / `_byCid`) that back
3
+ * `hasApplogCid` / `getApplog` (and thus `hasApplog`, `insertMissing`).
4
+ *
5
+ * These caches are built lazily on first read and MUST be invalidated on every
6
+ * `_applogs` mutation. This suite pins that invariant on each mutation/composition
7
+ * path so a future refactor (e.g. re-adding a naive local cache to a shared-array
8
+ * wrapper) fails loudly instead of silently serving stale membership.
9
+ *
10
+ * The pattern in each test is deliberately: read (build cache) → mutate → read again.
11
+ * A missing invalidation only surfaces when the cache was populated BEFORE the mutation.
12
+ */
13
+ import { describe, expect, it } from 'vitest'
14
+ import { finalizeApplogForInsert, joinThreads } from '../applog/applog-helpers.ts'
15
+ import type { Applog, ApplogForInsert } from '../applog/datom-types.ts'
16
+ import { MappedThread } from './mapped.ts'
17
+ import { ThreadInMemory } from './writeable.ts'
18
+
19
+ let idSeq = 0
20
+ /** A valid, unique Applog with a real CID. */
21
+ function makeLog(en = `e-${idSeq}`): Applog {
22
+ const id = idSeq++
23
+ return finalizeApplogForInsert(
24
+ { en, at: 'v', vl: id, ts: new Date(1700000000000 + id * 1000).toISOString(), pv: null, ag: 'a' } as ApplogForInsert,
25
+ {},
26
+ )
27
+ }
28
+
29
+ describe('membership cache invalidation', () => {
30
+ it('WriteableThread.insertRaw makes new cids visible (was cache built before insert)', () => {
31
+ const [a, b] = [makeLog(), makeLog()]
32
+ const t = ThreadInMemory.fromArray([a], 'insertRaw')
33
+
34
+ expect(t.hasApplogCid(b.cid)).toBe(false) // build cache while b absent
35
+ expect(t.getApplog(b.cid)).toBeUndefined()
36
+
37
+ t.insertRaw([b])
38
+
39
+ expect(t.hasApplogCid(b.cid)).toBe(true)
40
+ expect(t.getApplog(b.cid)).toBe(b)
41
+ })
42
+
43
+ it('WriteableThread.insert (finalize path) invalidates too', () => {
44
+ const t = ThreadInMemory.fromArray([makeLog()], 'insert')
45
+ expect(t.hasApplogCid('bogus-cid')).toBe(false) // build cache
46
+ const before = t.size
47
+ t.insert([{ en: 'new-entity', at: 'v', vl: 1, ag: 'a' } as ApplogForInsert])
48
+ expect(t.size).toBe(before + 1)
49
+ const inserted = t.applogs[t.applogs.length - 1]
50
+ expect(t.hasApplogCid(inserted.cid)).toBe(true)
51
+ expect(t.getApplog(inserted.cid)).toBe(inserted)
52
+ })
53
+
54
+ it('WriteableThread.purge removes cids from the cache (purge does not notify)', () => {
55
+ const [a, b] = [makeLog(), makeLog()]
56
+ const t = ThreadInMemory.fromArray([a, b], 'purge')
57
+
58
+ expect(t.hasApplogCid(b.cid)).toBe(true) // build cache while b present
59
+ expect(t.getApplog(b.cid)).toBe(b)
60
+
61
+ t.purge([b.cid])
62
+
63
+ expect(t.hasApplogCid(b.cid)).toBe(false)
64
+ expect(t.getApplog(b.cid)).toBeUndefined()
65
+ expect(t.hasApplogCid(a.cid)).toBe(true) // untouched cid still present
66
+ })
67
+
68
+ it('insertMissing leaves the cache consistent', () => {
69
+ const [a, b, c] = [makeLog(), makeLog(), makeLog()]
70
+ const t = ThreadInMemory.fromArray([a], 'insertMissing')
71
+ expect(t.hasApplogCid(b.cid)).toBe(false) // build cache
72
+ const inserted = t.insertMissing([a, b, c]) // a is dup, b & c are new
73
+ expect(inserted).toHaveLength(2)
74
+ expect(t.hasApplogCid(b.cid)).toBe(true)
75
+ expect(t.hasApplogCid(c.cid)).toBe(true)
76
+ })
77
+
78
+ describe('derived MappedThread (owns its array, self-invalidating cache)', () => {
79
+ it('reflects inserts routed through a parent (joinThreads)', () => {
80
+ const [a, b, fresh] = [makeLog(), makeLog(), makeLog()]
81
+ const pa = ThreadInMemory.fromArray([a], 'A')
82
+ const pb = ThreadInMemory.fromArray([b], 'B')
83
+ const joined = joinThreads([pa, pb])
84
+
85
+ expect(joined.hasApplogCid(fresh.cid)).toBe(false) // build joined cache
86
+ expect(joined.hasApplogCid(a.cid)).toBe(true)
87
+
88
+ pa.insertRaw([fresh]) // parent delta → joined.onParentUpdate → must invalidate joined cache
89
+
90
+ expect(joined.hasApplogCid(fresh.cid)).toBe(true)
91
+ expect(joined.getApplog(fresh.cid)).toBe(fresh)
92
+ })
93
+ })
94
+
95
+ describe('shared-array MappedThread (delegates to parent, no local cache)', () => {
96
+ it('asReadOnly wrapper sees parent inserts through delegation', () => {
97
+ const [a, fresh] = [makeLog(), makeLog()]
98
+ const parent = ThreadInMemory.fromArray([a], 'parent')
99
+ const ro = MappedThread.asReadOnly(parent) as MappedThread
100
+
101
+ expect(ro.hasApplogCid(fresh.cid)).toBe(false) // populate whatever cache the wrapper would keep
102
+ expect(ro.getApplog(fresh.cid)).toBeUndefined()
103
+
104
+ parent.insertRaw([fresh]) // mutates the SHARED array; wrapper is never notified
105
+
106
+ expect(ro.hasApplogCid(fresh.cid)).toBe(true) // only correct via delegation to parent
107
+ expect(ro.getApplog(fresh.cid)).toBe(fresh)
108
+ })
109
+
110
+ it('nested wrapper chain still delegates to the true owner', () => {
111
+ const [a, fresh] = [makeLog(), makeLog()]
112
+ const parent = ThreadInMemory.fromArray([a], 'owner')
113
+ const ro1 = MappedThread.asReadOnly(parent) as MappedThread
114
+ const ro2 = MappedThread.asReadOnly(ro1) as MappedThread
115
+
116
+ expect(ro2.hasApplogCid(fresh.cid)).toBe(false)
117
+ parent.insertRaw([fresh])
118
+ expect(ro2.hasApplogCid(fresh.cid)).toBe(true)
119
+ expect(ro2.getApplog(fresh.cid)).toBe(fresh)
120
+ })
121
+
122
+ it('mapWrites wrapper reflects parent state after purge', () => {
123
+ const [a, b] = [makeLog(), makeLog()]
124
+ const parent = ThreadInMemory.fromArray([a, b], 'parent')
125
+ const wrapped = MappedThread.mapWrites(parent, 'Pass', (logs) => logs) as MappedThread
126
+
127
+ expect(wrapped.hasApplogCid(b.cid)).toBe(true) // build via delegation
128
+ parent.purge([b.cid])
129
+ expect(wrapped.hasApplogCid(b.cid)).toBe(false)
130
+ expect(wrapped.getApplog(b.cid)).toBeUndefined()
131
+ })
132
+ })
133
+ })
@@ -20,6 +20,7 @@ export abstract class WriteableThread extends Thread {
20
20
  const beforeCount = this.applogs.length
21
21
  // HACK setting readonly
22
22
  this._applogs = this.applogs.filter(log => !(cidsToPurge.includes(log.cid)))
23
+ this.invalidateApplogCaches() // purge doesn't notifySubscribers, so invalidate the cid caches directly
23
24
  return beforeCount - this.applogs.length
24
25
  }
25
26
  public insert(appLogsToInsert: ApplogForInsert[]) {
@@ -138,7 +139,7 @@ export class ThreadInMemory extends WriteableThread {
138
139
  }
139
140
  static fromReadOnlyArray(applogs: readonly Applog[], name?: string) {
140
141
  // @ts-expect-error readonly conditional drama
141
- return new ThreadInMemory(name ?? 'in-memory', applogs, [], true)
142
+ return new ThreadInMemory(name ?? 'in-memory', applogs, [], true)
142
143
  }
143
144
 
144
145
  constructor(