Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { tryReference, types } from "mobx-state-tree";
import Types from "../../core/Types";
import { SharedStoreModel } from "./model";
 
/**
 * StoreIds and Stores act as a cache.
 *
 * The reason behind those is that we're creating a new store on the `preProcessSnapshot` when there's no
 * access to the State Tree. When the store is created, it's put into the cache and retrieved back in the
 * `afterCreate` hook of the model.
 *
 * StoreIds is just a map of existing store IDs to reference to during the `preProcessSnapshot`.
 */
export const Stores = new Map();
const StoreIds = new Set();
 
/**
 * Defines the ID to group SharedStores by.
 */
const SharedStoreID = types.optional(types.maybeNull(types.string), null);
 
/**
 * Defines the Store model referenced from the Annotation Store
 */
const Store = types.optional(types.maybeNull(types.late(() => types.reference(SharedStoreModel))), null);
 
/**
 * SharedStoreMixin, when injected into the model, provides an AnnotationStore level shared storages to
 * reduce the memory footprint and computation time.
 *
 * It was specifically designed to be used with Repeater tag where the memory issues are the most sound.
 *
 * This mixin provides a `sharedStore` property to the model which is a reference to the shared store.
 *
 * The concept behind it is that whenever a model is parsing a snapshot, children are subtracted from the
 * initial snapshot, and put into the newly created SharedStore.
 *
 * The store is then put into the cache and attached to the model in the `afterCreate` hook. Any subsequent
 * models lookup the store in the cache first and use its id instead of creating a new one.
 *
 * When the store is fullfilled with children, it's locked and cannot be modified anymore. The allows the model
 * not to process children anymore and just use the store.
 *
 * Shared Stores live on the AnnotationStore level meaning that even if the user switches between annotations or
 * create new ones, they will all use the same shared store decreasing the memory footprint and computation time.
 */
export const SharedStoreMixin = types
  .model("SharedStoreMixin", {
    sharedstore: SharedStoreID,
    store: Store,
  })
  .views((self) => ({
    get children() {
      return self.sharedChildren;
    },
 
    get locked() {
      return self.store?.locked ?? false;
    },
 
    set children(val) {
      self.store?.lock();
      self.store.setChildren(val);
    },
 
    get sharedChildren() {
      return self.store.children ?? [];
    },
 
    get storeId() {
      return self.sharedstore ?? self.name;
    },
  }))
  .actions((self) => ({
    afterCreate() {
      const currentStore = tryReference(() => self.store);
 
      if (!currentStore) {
        const store = Stores.get(self.storeId);
        const annotationStore = Types.getParentOfTypeString(self, "AnnotationStore");
 
        // It means that an element is not connected to the store tree,
        // most probably as it is a temporal clone of the model
        if (!annotationStore) return;
        annotationStore.addSharedStore(store);
        StoreIds.add(self.storeId);
        self.store = self.storeId;
      }
    },
  }))
  .preProcessSnapshot((sn) => {
    const storeId = sn.sharedstore ?? sn.name;
 
    if (StoreIds.has(storeId)) {
      sn.store = storeId;
    } else {
      Stores.set(
        storeId,
        SharedStoreModel.create({
          id: storeId,
          children: sn._children ?? sn.children ?? [],
        }),
      );
    }
 
    return sn;
  });
 
export const destroy = () => {
  Stores.clear();
  StoreIds.clear();
};