atom.io 0.42.0 → 0.42.2

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.
@@ -5,7 +5,7 @@ import type {
5
5
  UpdateHandler,
6
6
  } from "atom.io"
7
7
 
8
- import { newest } from "../lineage"
8
+ import { eldest, newest } from "../lineage"
9
9
  import { createStandaloneSelector } from "../selector"
10
10
  import { resetInStore, setIntoStore } from "../set-state"
11
11
  import type { MutableAtom } from "../state-types"
@@ -58,6 +58,18 @@ export function createMutableAtom<T extends Transceiver<any, any, any>>(
58
58
  }
59
59
  target.atoms.set(newAtom.key, newAtom)
60
60
  const token = deposit(newAtom)
61
+
62
+ new Tracker(token, store)
63
+ if (!family) {
64
+ createStandaloneSelector(store, {
65
+ key: `${key}:JSON`,
66
+ get: ({ get }) => get(token).toJSON(),
67
+ set: ({ set }, newValue) => {
68
+ set(token, options.class.fromJSON(newValue))
69
+ },
70
+ })
71
+ }
72
+
61
73
  if (options.effects) {
62
74
  let effectIndex = 0
63
75
  const cleanupFunctions: (() => void)[] = []
@@ -71,6 +83,8 @@ export function createMutableAtom<T extends Transceiver<any, any, any>>(
71
83
  },
72
84
  onSet: (handle: UpdateHandler<T>) =>
73
85
  subscribeToState(store, token, `effect[${effectIndex}]`, handle),
86
+ token: token as any,
87
+ store: eldest(store),
74
88
  })
75
89
  if (cleanup) {
76
90
  cleanupFunctions.push(cleanup)
@@ -84,16 +98,6 @@ export function createMutableAtom<T extends Transceiver<any, any, any>>(
84
98
  }
85
99
  }
86
100
 
87
- new Tracker(token, store)
88
- if (!family) {
89
- createStandaloneSelector(store, {
90
- key: `${key}:JSON`,
91
- get: ({ get }) => get(token).toJSON(),
92
- set: ({ set }, newValue) => {
93
- set(token, options.class.fromJSON(newValue))
94
- },
95
- })
96
- }
97
101
  store.on.atomCreation.next(token)
98
102
  return token
99
103
  }
@@ -107,20 +107,7 @@ export class Store implements Lineage {
107
107
  new CircularBuffer(100)
108
108
 
109
109
  public molecules: Map<string, Molecule<Canonical>> = new Map()
110
- public moleculeJoins: Junction<
111
- `moleculeKey`,
112
- stringified<Canonical>,
113
- `joinKey`,
114
- string
115
- > = new Junction(
116
- {
117
- between: [`moleculeKey`, `joinKey`],
118
- cardinality: `n:n`,
119
- },
120
- {
121
- makeContentKey: (...keys) => keys.sort().join(`:`),
122
- },
123
- )
110
+
124
111
  public moleculeGraph: Junction<
125
112
  `upstreamMoleculeKey`,
126
113
  stringified<Canonical> | `"root"`,
@@ -150,6 +137,20 @@ export class Store implements Lineage {
150
137
  makeContentKey: (...keys) => keys.sort().join(`:`),
151
138
  },
152
139
  )
140
+ public keyRefsInJoins: Junction<
141
+ `moleculeKey`,
142
+ stringified<Canonical>,
143
+ `joinKey`,
144
+ string
145
+ > = new Junction(
146
+ {
147
+ between: [`moleculeKey`, `joinKey`],
148
+ cardinality: `n:n`,
149
+ },
150
+ {
151
+ makeContentKey: (...keys) => keys.sort().join(`:`),
152
+ },
153
+ )
153
154
  public miscResources: Map<string, Disposable> = new Map()
154
155
 
155
156
  public on: StoreEventCarrier = {
@@ -54,7 +54,7 @@ export const buildTransaction = (
54
54
  molecules: new MapOverlay(parent.molecules),
55
55
  moleculeGraph: parent.moleculeGraph.overlay(),
56
56
  moleculeData: parent.moleculeData.overlay(),
57
- moleculeJoins: parent.moleculeJoins.overlay(),
57
+ keyRefsInJoins: parent.keyRefsInJoins.overlay(),
58
58
  miscResources: new MapOverlay(parent.miscResources),
59
59
  }
60
60
  const epoch = getEpochNumberOfAction(store, token.key)
package/src/main/atom.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ConstructorOf, Ctor, Transceiver } from "atom.io/internal"
1
+ import type { ConstructorOf, Ctor, Store, Transceiver } from "atom.io/internal"
2
2
  import {
3
3
  createMutableAtom,
4
4
  createMutableAtomFamily,
@@ -10,6 +10,7 @@ import type { Canonical } from "atom.io/json"
10
10
 
11
11
  import type { StateUpdate } from "./events"
12
12
  import type {
13
+ AtomToken,
13
14
  MutableAtomFamilyToken,
14
15
  MutableAtomToken,
15
16
  RegularAtomFamilyToken,
@@ -22,7 +23,7 @@ export type RegularAtomOptions<T, E = never> = {
22
23
  /** The starting value of the atom */
23
24
  default: T | (() => T)
24
25
  /** Hooks used to run side effects when the atom is set */
25
- effects?: readonly AtomEffect<T>[]
26
+ effects?: readonly AtomEffect<T, E>[]
26
27
  /** The classes of errors that might be thrown when deriving the atom's default value */
27
28
  catch?: readonly Ctor<E>[]
28
29
  }
@@ -67,8 +68,10 @@ export function mutableAtom<T extends Transceiver<any, any, any>>(
67
68
  * @returns
68
69
  * Optionally, a cleanup function that will be called when the atom is disposed
69
70
  */
70
- export type AtomEffect<T> = (tools: Effectors<T>) => (() => void) | void
71
- export type Effectors<T> = {
71
+ export type AtomEffect<T, E = never> = (
72
+ tools: Effectors<T, E>,
73
+ ) => (() => void) | void
74
+ export type Effectors<T, E = never> = {
72
75
  /**
73
76
  * Reset the value of the atom to its default
74
77
  */
@@ -79,7 +82,13 @@ export type Effectors<T> = {
79
82
  */
80
83
  setSelf: <New extends T>(next: New | ((old: T) => New)) => void
81
84
  /** Subscribe to changes to the atom */
82
- onSet: (callback: (options: StateUpdate<T>) => void) => void
85
+ onSet: (callback: (options: StateUpdate<E | T>) => void) => void
86
+ /** The token of the atom */
87
+ token: T extends Transceiver<any, any, any>
88
+ ? MutableAtomToken<T>
89
+ : AtomToken<T, any, E>
90
+ /** The store in which the atom exists */
91
+ store: Store
83
92
  }
84
93
 
85
94
  export type RegularAtomFamilyOptions<T, K extends Canonical, E = never> = {
@@ -88,7 +97,7 @@ export type RegularAtomFamilyOptions<T, K extends Canonical, E = never> = {
88
97
  /** The starting value of the atom family */
89
98
  default: T | ((key: K) => T)
90
99
  /** Hooks used to run side effects when an atom in the family is set */
91
- effects?: (key: K) => AtomEffect<T>[]
100
+ effects?: (key: K) => AtomEffect<T, E>[]
92
101
  /** The classes of errors that might be thrown when deriving the atom's default value */
93
102
  catch?: readonly Ctor<E>[]
94
103
  }
package/src/main/realm.ts CHANGED
@@ -14,16 +14,20 @@ import type { Canonical } from "atom.io/json"
14
14
 
15
15
  import type { TransactionToken } from "./tokens"
16
16
 
17
- export const $claim: unique symbol = Symbol.for(`claim`)
18
- export type Claim<K extends Canonical> = K & { [$claim]?: true }
17
+ export const $validatedKey: unique symbol = Symbol.for(`claim`)
18
+ export type ValidKey<K extends Canonical> = K & { [$validatedKey]?: true }
19
+
20
+ export function simpleCompound(a: string, b: string): string {
21
+ return [a, b].sort().join(`\u001F`)
22
+ }
19
23
 
20
24
  export class Realm<H extends Hierarchy> {
21
25
  public store: RootStore
22
- public deallocateTX: TransactionToken<(claim: Claim<Vassal<H>>) => void>
26
+ public deallocateTX: TransactionToken<(claim: ValidKey<Vassal<H>>) => void>
23
27
  public claimTX: TransactionToken<
24
28
  <V extends Exclude<Vassal<H>, CompoundTypedKey>, A extends Above<V, H>>(
25
29
  newProvenance: A,
26
- claim: Claim<V>,
30
+ claim: ValidKey<V>,
27
31
  exclusive?: `exclusive`,
28
32
  ) => void
29
33
  >
@@ -42,13 +46,13 @@ export class Realm<H extends Hierarchy> {
42
46
  * @param key - A unique identifier for the new subject
43
47
  * @param attachmentStyle - The attachment style of new subject to its owner(s). `any` means that if any owners remain, the subject will be retained. `all` means that the subject be retained only if all owners remain .
44
48
  * @returns
45
- * The subject's key, given status as a true {@link Claim}
49
+ * The subject's key, given status as a true {@link ValidKey}
46
50
  */
47
51
  public allocate<V extends Vassal<H>, A extends Above<V, H>>(
48
52
  provenance: A,
49
53
  key: V,
50
54
  attachmentStyle?: `all` | `any`,
51
- ): Claim<V> {
55
+ ): ValidKey<V> {
52
56
  return allocateIntoStore<H, V, A>(
53
57
  this.store,
54
58
  provenance,
@@ -62,7 +66,7 @@ export class Realm<H extends Hierarchy> {
62
66
  * @param reagentA - the left reagent of the compound
63
67
  * @param reagentB - the right reagent of the compound
64
68
  * @returns
65
- * The compound's key, given status as a true {@link Claim}
69
+ * The compound's key, given status as a {@link ValidKey}
66
70
  */
67
71
  public fuse<
68
72
  C extends CompoundFrom<H>,
@@ -73,14 +77,14 @@ export class Realm<H extends Hierarchy> {
73
77
  type: T,
74
78
  reagentA: SingularTypedKey<A>,
75
79
  reagentB: SingularTypedKey<B>,
76
- ): Claim<CompoundTypedKey<T, A, B>> {
80
+ ): ValidKey<CompoundTypedKey<T, A, B>> {
77
81
  return fuseWithinStore<H, C, T, A, B>(this.store, type, reagentA, reagentB)
78
82
  }
79
83
  /**
80
84
  * Remove a subject from the realm
81
85
  * @param claim - The subject to be deallocated
82
86
  */
83
- public deallocate<V extends Vassal<H>>(claim: Claim<V>): void {
87
+ public deallocate<V extends Vassal<H>>(claim: ValidKey<V>): void {
84
88
  actUponStore(this.store, this.deallocateTX, arbitrary())(claim)
85
89
  }
86
90
  /**
@@ -89,12 +93,12 @@ export class Realm<H extends Hierarchy> {
89
93
  * @param claim - The subject to be claimed
90
94
  * @param exclusive - Whether the subjects previous owners should be detached from it
91
95
  * @returns
92
- * The subject's key, given status as a true {@link Claim}
96
+ * The subject's key, given status as a true {@link ValidKey}
93
97
  */
94
98
  public claim<
95
99
  V extends Exclude<Vassal<H>, CompoundTypedKey>,
96
100
  A extends Above<V, H>,
97
- >(newProvenance: A, claim: Claim<V>, exclusive?: `exclusive`): void {
101
+ >(newProvenance: A, claim: ValidKey<V>, exclusive?: `exclusive`): void {
98
102
  actUponStore(this.store, this.claimTX, arbitrary())(
99
103
  newProvenance,
100
104
  claim,
@@ -157,10 +161,47 @@ export class Anarchy {
157
161
  ): void {
158
162
  claimWithinStore<any, any, any>(this.store, newProvenance, key, exclusive)
159
163
  }
164
+ /**
165
+ * Fuse two reagents into a compound
166
+ * @param type - the name of the compound that is being fused
167
+ * @param reagentA - the left reagent of the compound
168
+ * @param reagentB - the right reagent of the compound
169
+ * @returns
170
+ * The compound's key, given status as a {@link ValidKey}
171
+ */
172
+ public fuse<T extends string, A extends string, B extends string>(
173
+ type: T,
174
+ reagentA: SingularTypedKey<A>,
175
+ reagentB: SingularTypedKey<B>,
176
+ ): ValidKey<CompoundTypedKey<T, A, B>> {
177
+ return fuseWithinStore<any, any, T, A, B>(
178
+ this.store,
179
+ type,
180
+ reagentA,
181
+ reagentB,
182
+ )
183
+ }
184
+ }
185
+
186
+ export function decomposeCompound(
187
+ compound: Canonical,
188
+ ): [type: string, a: string, b: string] | null {
189
+ if ((typeof compound === `string`) === false) {
190
+ return null
191
+ }
192
+ const [typeTag, components] = compound.split(`==`)
193
+ if (!components) {
194
+ return null
195
+ }
196
+ const type = typeTag.slice(4)
197
+ const [a, b] = components.split(`++`)
198
+ if (type && a && b) {
199
+ return [type, a, b]
200
+ }
201
+ return null
160
202
  }
161
203
 
162
- export const T$ = `T$`
163
- export type T$ = typeof T$
204
+ export type T$ = `T$`
164
205
  export type TypeTag<T extends string> = `${T$}--${T}`
165
206
  export type SingularTypedKey<T extends string = string> = `${T}::${string}`
166
207
  export type CompoundTypedKey<
@@ -1 +1,2 @@
1
1
  export * from "./o-list"
2
+ export * from "./o-list-disposed-key-cleanup-effect"
@@ -0,0 +1,153 @@
1
+ import type { AtomEffect } from "atom.io"
2
+ import { getFromStore, getUpdateToken, subscribeInStore } from "atom.io/internal"
3
+ import { type primitive, stringifyJson } from "atom.io/json"
4
+
5
+ import { OList } from "./o-list"
6
+
7
+ export function filterOutInPlace<T>(arr: T[], toRemove: T): T[] {
8
+ let writeIndex = 0
9
+
10
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
11
+ for (let readIndex = 0; readIndex < arr.length; readIndex++) {
12
+ if (toRemove !== arr[readIndex]) {
13
+ arr[writeIndex] = arr[readIndex]
14
+ writeIndex++
15
+ }
16
+ }
17
+
18
+ arr.length = writeIndex
19
+ return arr
20
+ }
21
+
22
+ export const oListDisposedKeyCleanupEffect: AtomEffect<OList<primitive>> = ({
23
+ token,
24
+ setSelf,
25
+ store,
26
+ }) => {
27
+ const disposalSubscriptions = new Map<primitive, () => void>()
28
+ const updateToken = getUpdateToken(token)
29
+
30
+ const addedValues = new Set<primitive>()
31
+ const removedValues = new Set<primitive>()
32
+ function updateSubscriptions() {
33
+ for (const addedValue of addedValues) {
34
+ const molecule = store.molecules.get(stringifyJson(addedValue))
35
+ if (molecule) {
36
+ disposalSubscriptions.set(
37
+ addedValue,
38
+ molecule.subject.subscribe(token.key, () => {
39
+ disposalSubscriptions.get(addedValue)?.()
40
+ disposalSubscriptions.delete(addedValue)
41
+ setSelf((self) => {
42
+ filterOutInPlace(self, addedValue)
43
+ return self
44
+ })
45
+ }),
46
+ )
47
+ } else {
48
+ store.logger.warn(
49
+ `❌`,
50
+ token.type,
51
+ token.key,
52
+ `Added "${addedValue}" to ${token.key} but it has not been allocated.`,
53
+ )
54
+ }
55
+ }
56
+ for (const removedValue of removedValues) {
57
+ if (disposalSubscriptions.has(removedValue)) {
58
+ disposalSubscriptions.get(removedValue)?.()
59
+ disposalSubscriptions.delete(removedValue)
60
+ }
61
+ }
62
+ }
63
+ subscribeInStore(
64
+ store,
65
+ updateToken,
66
+ function manageAutoDeletionTriggers({ newValue }) {
67
+ const currentList = getFromStore(store, token)
68
+ const unpacked = OList.unpackUpdate(newValue)
69
+ switch (unpacked.type) {
70
+ case `extend`:
71
+ case `reverse`:
72
+ case `sort`:
73
+ break // these don't change what values are present in the list
74
+
75
+ case `set`:
76
+ {
77
+ const { next } = unpacked
78
+ if (`prev` in unpacked && !currentList.includes(unpacked.prev)) {
79
+ removedValues.add(unpacked.prev)
80
+ }
81
+ if (!disposalSubscriptions.has(next)) {
82
+ addedValues.add(next)
83
+ }
84
+ }
85
+ break
86
+ case `truncate`:
87
+ {
88
+ for (const item of unpacked.items) {
89
+ if (!currentList.includes(item)) {
90
+ removedValues.add(item)
91
+ }
92
+ }
93
+ }
94
+ break
95
+ case `shift`:
96
+ case `pop`:
97
+ {
98
+ const { value } = unpacked
99
+ if (value !== undefined && !currentList.includes(value)) {
100
+ removedValues.add(value)
101
+ }
102
+ }
103
+ break
104
+ case `push`:
105
+ case `unshift`:
106
+ for (const item of unpacked.items) {
107
+ if (!disposalSubscriptions.has(item)) {
108
+ addedValues.add(item)
109
+ }
110
+ }
111
+ break
112
+ case `copyWithin`:
113
+ for (const item of unpacked.prev) {
114
+ if (!currentList.includes(item)) {
115
+ removedValues.add(item)
116
+ }
117
+ }
118
+ break
119
+ case `fill`:
120
+ {
121
+ const { value } = unpacked
122
+ if (value !== undefined) {
123
+ if (!disposalSubscriptions.has(value)) {
124
+ addedValues.add(value)
125
+ }
126
+ }
127
+ for (const item of unpacked.prev) {
128
+ if (!currentList.includes(item)) {
129
+ removedValues.add(item)
130
+ }
131
+ }
132
+ }
133
+ break
134
+ case `splice`:
135
+ for (const item of unpacked.deleted) {
136
+ if (!currentList.includes(item)) {
137
+ removedValues.add(item)
138
+ }
139
+ }
140
+ for (const addedItem of unpacked.items) {
141
+ if (!disposalSubscriptions.has(addedItem)) {
142
+ addedValues.add(addedItem)
143
+ }
144
+ }
145
+ break
146
+ }
147
+ updateSubscriptions()
148
+ addedValues.clear()
149
+ removedValues.clear()
150
+ },
151
+ `set-auto-deletion-triggers`,
152
+ )
153
+ }
@@ -1 +1,2 @@
1
1
  export * from "./u-list"
2
+ export * from "./u-list-disposed-key-cleanup-effect"
@@ -0,0 +1,47 @@
1
+ import type { AtomEffect } from "atom.io"
2
+ import { getUpdateToken, subscribeInStore } from "atom.io/internal"
3
+ import { type primitive, stringifyJson } from "atom.io/json"
4
+
5
+ import { UList } from "./u-list"
6
+
7
+ export const uListDisposedKeyCleanupEffect: AtomEffect<UList<primitive>> = ({
8
+ token,
9
+ setSelf,
10
+ store,
11
+ }) => {
12
+ const disposalSubscriptions = new Map<primitive, () => void>()
13
+ const updateToken = getUpdateToken(token)
14
+ subscribeInStore(
15
+ store,
16
+ updateToken,
17
+ function setAutoDeletionTriggers({ newValue }) {
18
+ const unpacked = UList.unpackUpdate(newValue)
19
+ switch (unpacked.type) {
20
+ case `add`:
21
+ {
22
+ const molecule = store.molecules.get(stringifyJson(unpacked.value))
23
+ if (molecule) {
24
+ disposalSubscriptions.set(
25
+ unpacked.value,
26
+ molecule.subject.subscribe(token.key, () => {
27
+ setSelf((self) => {
28
+ self.delete(unpacked.value)
29
+ return self
30
+ })
31
+ }),
32
+ )
33
+ }
34
+ }
35
+ break
36
+ case `delete`:
37
+ disposalSubscriptions.get(unpacked.value)?.()
38
+ disposalSubscriptions.delete(unpacked.value)
39
+ break
40
+ case `clear`:
41
+ for (const unsub of disposalSubscriptions.values()) unsub()
42
+ disposalSubscriptions.clear()
43
+ }
44
+ },
45
+ `set-auto-deletion-triggers`,
46
+ )
47
+ }