Skip to content

Commit

Permalink
more changes still wip
Browse files Browse the repository at this point in the history
  • Loading branch information
dmaskasky committed Sep 24, 2024
1 parent 4ba79f1 commit ee7ca84
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 316 deletions.
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": false
"semi": false,
"printWidth": 100
}
2 changes: 1 addition & 1 deletion jotai/vanilla/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type Mounted = {
* Mutable atom state,
* tracked for both mounted and unmounted atoms in a store.
*/
export type AtomState<Value = AnyValue> = {
type AtomState<Value = AnyValue> = {
/**
* Map of atoms that the atom depends on.
* The map value is the epoch number of the dependency.
Expand Down
27 changes: 27 additions & 0 deletions notes
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
computed atoms are like implicit atoms but reverse
computed atoms are not copied
reverseImplicitSet is a set of computed atoms to indicate whether a computed atom should be treated as a reverse implicit
the atom is removed from the set between recomputations
by intercepting the readFn, if an atom is `get` that is either an explicit or reverse implicit,

- then the atom is added to the reverse implicit set

only the readFn determines if the atom is added to the reverse implicit set
intercepting the readFn and writeFn is used to get the "correct" atom
when a computed atom converts to reverse implicit,

- its atomState is copied from the unscoped atomState
- this is because the atomState stores a different value for the scoped atom and can have different dependencies

**Special Case:** on first read, when a computed atom reads a scoped atom,

1. it is added to the reverse implicit set
1. the atomState is copied from the unscoped atomState
1. getAtomState points to the scoped atomState

the atomStateProxy is no longer needed.

# Implementation

readAtomTrap:
getter:
138 changes: 77 additions & 61 deletions src/ScopeProvider/scope.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { atom, type Atom } from 'jotai';
import type { AnyAtomFamily, AnyAtom, AnyWritableAtom, Scope } from './types';
import { atom, type Atom } from 'jotai'
import type { AnyAtomFamily, AnyAtom, AnyWritableAtom, Scope } from './types'

const globalScopeKey: { name?: string } = {};
const globalScopeKey: { name?: string } = {}
if (process.env.NODE_ENV !== 'production') {
globalScopeKey.name = 'unscoped';
globalScopeKey.toString = toString;
globalScopeKey.name = 'unscoped'
globalScopeKey.toString = toString
}

type GlobalScopeKey = typeof globalScopeKey;
type GlobalScopeKey = typeof globalScopeKey

export function createScope(
atoms: Set<AnyAtom>,
atomFamilies: Set<AnyAtomFamily>,
parentScope: Scope | undefined,
scopeName?: string | undefined,
): Scope {
const explicit = new WeakMap<AnyAtom, [AnyAtom, Scope?]>();
const implicit = new WeakMap<AnyAtom, [AnyAtom, Scope?]>();
type ScopeMap = WeakMap<AnyAtom, [AnyAtom, Scope?]>;
const inherited = new WeakMap<Scope | GlobalScopeKey, ScopeMap>();
const explicit = new WeakMap<AnyAtom, [AnyAtom, Scope?]>()
const implicit = new WeakMap<AnyAtom, [AnyAtom, Scope?]>()
type ScopeMap = WeakMap<AnyAtom, [AnyAtom, Scope?]>
const inherited = new WeakMap<Scope | GlobalScopeKey, ScopeMap>()

const currentScope: Scope = {
getAtom,
Expand All @@ -34,75 +34,76 @@ export function createScope(
// atom is writable with init and holds a value
// we need to preserve the value, so we don't want to copy the atom
// instead, we need to override write until the write is finished
const { write } = originalAtom;
const { write } = originalAtom
anAtom.write = createScopedWrite(
originalAtom.write.bind(
originalAtom,
) as (typeof originalAtom)['write'],
implicitScope,
);
)
return () => {
anAtom.write = write;
};
anAtom.write = write
}
}
return undefined;
return undefined
},
};
}

if (scopeName && process.env.NODE_ENV !== 'production') {
currentScope.name = scopeName;
currentScope.toString = toString;
currentScope.name = scopeName
currentScope.toString = toString
}

// populate explicitly scoped atoms
for (const anAtom of atoms) {
explicit.set(anAtom, [cloneAtom(anAtom, currentScope), currentScope]);
explicit.set(anAtom, [cloneAtom(anAtom, currentScope), currentScope])
}

const cleanupFamiliesSet = new Set<() => void>();
const cleanupFamiliesSet = new Set<() => void>()
for (const atomFamily of atomFamilies) {
for (const param of atomFamily.getParams()) {
const anAtom = atomFamily(param);
const anAtom = atomFamily(param)
if (!explicit.has(anAtom)) {
explicit.set(anAtom, [cloneAtom(anAtom, currentScope), currentScope]);
explicit.set(anAtom, [cloneAtom(anAtom, currentScope), currentScope])
}
}
const cleanupFamily = atomFamily.unstable_listen((e) => {
if (e.type === 'CREATE' && !explicit.has(e.atom)) {
explicit.set(e.atom, [cloneAtom(e.atom, currentScope), currentScope]);
explicit.set(e.atom, [cloneAtom(e.atom, currentScope), currentScope])
} else if (!atoms.has(e.atom)) {
explicit.delete(e.atom);
explicit.delete(e.atom)
}
});
cleanupFamiliesSet.add(cleanupFamily);
})
cleanupFamiliesSet.add(cleanupFamily)
}
currentScope.cleanup = combineVoidFunctions(
currentScope.cleanup,
...Array.from(cleanupFamiliesSet),
);
)

/**
* Returns a scoped atom from the original atom.
* @param anAtom
* @param implicitScope the atom is implicitly scoped in the provided scope
* - when the implicit scope is the current scope, the atom is emplaced in the implicit set and returned
* @returns the scoped atom and the scope of the atom
*/
function getAtom<T extends AnyAtom>(
anAtom: T,
implicitScope?: Scope,
): [T, Scope?] {
if (explicit.has(anAtom)) {
return explicit.get(anAtom) as [T, Scope];
return explicit.get(anAtom) as [T, Scope]
}
if (implicitScope === currentScope) {
// dependencies of explicitly scoped atoms are implicitly scoped
// implicitly scoped atoms are only accessed by implicit and explicit scoped atoms
if (!implicit.has(anAtom)) {
implicit.set(anAtom, [cloneAtom(anAtom, implicitScope), implicitScope]);
implicit.set(anAtom, [cloneAtom(anAtom, implicitScope), implicitScope])
}
return implicit.get(anAtom) as [T, Scope];
return implicit.get(anAtom) as [T, Scope]
}
const scopeKey = implicitScope ?? globalScopeKey;
const scopeKey = implicitScope ?? globalScopeKey
if (parentScope) {
// inherited atoms are copied so they can access scoped atoms
// but they are not explicitly scoped
Expand All @@ -112,22 +113,22 @@ export function createScope(
const [ancestorAtom, explicitScope] = parentScope.getAtom(
anAtom,
implicitScope,
);
)
setInheritedAtom(
inheritAtom(ancestorAtom, anAtom, explicitScope),
anAtom,
implicitScope,
explicitScope,
);
)
}
return inherited.get(scopeKey)!.get(anAtom) as [T, Scope];
return inherited.get(scopeKey)!.get(anAtom) as [T, Scope]
}
if (!inherited.get(scopeKey)?.has(anAtom)) {
// non-primitive atoms may need to access scoped atoms
// so we need to create a copy of the atom
setInheritedAtom(inheritAtom(anAtom, anAtom), anAtom);
setInheritedAtom(inheritAtom(anAtom, anAtom), anAtom)
}
return inherited.get(scopeKey)!.get(anAtom) as [T, Scope?];
return inherited.get(scopeKey)!.get(anAtom) as [T, Scope?]
}

function setInheritedAtom<T extends AnyAtom>(
Expand All @@ -136,17 +137,17 @@ export function createScope(
implicitScope?: Scope,
explicitScope?: Scope,
) {
const scopeKey = implicitScope ?? globalScopeKey;
const scopeKey = implicitScope ?? globalScopeKey
if (!inherited.has(scopeKey)) {
inherited.set(scopeKey, new WeakMap());
inherited.set(scopeKey, new WeakMap())
}
inherited.get(scopeKey)!.set(
originalAtom,
[
scopedAtom, //
explicitScope,
].filter(Boolean) as [T, Scope?],
);
)
}

/**
Expand All @@ -158,26 +159,29 @@ export function createScope(
implicitScope?: Scope,
) {
if (originalAtom.read !== defaultRead) {
return cloneAtom(originalAtom, implicitScope);
return cloneAtom(originalAtom, implicitScope)
}
return anAtom;
return anAtom
}

/**
* Makes a clone of the atom
* - replaces read with a scoped read function
* - replaces write with a scoped write function
* @returns a scoped copy of the atom
*/
function cloneAtom<T>(originalAtom: Atom<T>, implicitScope?: Scope) {
// avoid reading `init` to preserve lazy initialization
const scopedAtom: Atom<T> = Object.create(
Object.getPrototypeOf(originalAtom),
Object.getOwnPropertyDescriptors(originalAtom),
);
)

if (scopedAtom.read !== defaultRead) {
scopedAtom.read = createScopedRead<typeof scopedAtom>(
originalAtom.read.bind(originalAtom),
implicitScope,
);
)
}

if (
Expand All @@ -188,63 +192,75 @@ export function createScope(
scopedAtom.write = createScopedWrite(
originalAtom.write.bind(originalAtom),
implicitScope,
);
)
}

return scopedAtom;
return scopedAtom
}

/**
* Creates a scoped read function that intercepts the read function of the original atom
* to intercept the getter with the custom getAtom function
* @param implicitScope
* @returns
*/
function createScopedRead<T extends Atom<unknown>>(
read: T['read'],
implicitScope?: Scope,
): T['read'] {
return function scopedRead(get, opts) {
return read(
function scopedGet(a) {
const [scopedAtom] = getAtom(a, implicitScope);
return get(scopedAtom);
const [scopedAtom] = getAtom(a, implicitScope)
return get(scopedAtom)
}, //
opts,
);
};
)
}
}

/**
* Creates a scoped write function that intercepts the write function of the original atom
* to intercept the getter and setter with the custom getAtom function
* @param implicitScope
* @returns
*/
function createScopedWrite<T extends AnyWritableAtom>(
write: T['write'],
implicitScope?: Scope,
): T['write'] {
return function scopedWrite(get, set, ...args) {
return write(
function scopedGet(a) {
const [scopedAtom] = getAtom(a, implicitScope);
return get(scopedAtom);
const [scopedAtom] = getAtom(a, implicitScope)
return get(scopedAtom)
},
function scopedSet(a, ...v) {
const [scopedAtom] = getAtom(a, implicitScope);
return set(scopedAtom, ...v);
const [scopedAtom] = getAtom(a, implicitScope)
return set(scopedAtom, ...v)
},
...args,
);
};
)
}
}

return currentScope;
return currentScope
}

function isWritableAtom(anAtom: AnyAtom): anAtom is AnyWritableAtom {
return 'write' in anAtom;
return 'write' in anAtom
}

const { read: defaultRead, write: defaultWrite } = atom<unknown>(null);
const { read: defaultRead, write: defaultWrite } = atom<unknown>(null)

function toString(this: { name: string }) {
return this.name;
return this.name
}

function combineVoidFunctions(...fns: (() => void)[]) {
return function combinedFunctions() {
for (const fn of fns) {
fn();
fn()
}
};
}
}
Loading

0 comments on commit ee7ca84

Please sign in to comment.