Skip to content

Commit

Permalink
Merge pull request #13 from Olian04/11-change-internals-to-use-weak-r…
Browse files Browse the repository at this point in the history
…eferences

Change internals to use weak references
  • Loading branch information
Olian04 authored Nov 27, 2022
2 parents 7296f65 + 2528fa6 commit edd4ede
Show file tree
Hide file tree
Showing 34 changed files with 703 additions and 347 deletions.
8 changes: 8 additions & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { MochaOptions } = require('mocha');

/** @type {MochaOptions} */
module.exports = {
spec: ['./tests/**/*.test.ts'],
require: 'ts-node/register',
recursive: true,
};
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.gitignore
.editorconfig
.mocharc.js
rollup.config.mjs
tsconfig.json
.vscode
Expand Down
69 changes: 69 additions & 0 deletions demos/browser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simply Reactive</title>
</head>
<body>
<div>
<span id="out-a"></span>
*
<span id="out-b"></span>
=
<span id="out-product"></span>
</div>
<lable>
A:
<input id="in-a" />
</lable>
<lable>
B:
<input id="in-b" />
</lable>

<button id="destroy-btn">Destroy</button>
<button id="restore-btn">Restore</button>

<script src="../cdn/simply-reactive.js"></script>
<script>
const { createAtom, createSelector, createEffectGroup } = simplyReactive;

const A = createAtom({
default: 2,
});

const B = createAtom({
default: 3,
});

const Prod = createSelector({
get: () => A.get() * B.get(),
});

document.getElementById('in-a').value = A.get();
document.getElementById('in-a').addEventListener('change', (ev) => {
A.set(parseInt(ev.target.value, 10));
});

document.getElementById('in-b').value = B.get();
document.getElementById('in-b').addEventListener('change', (ev) => {
B.set(parseInt(ev.target.value, 10));
});

const Effect = createEffectGroup([
() => (document.getElementById('out-a').innerText = A.get()),
() => (document.getElementById('out-b').innerText = B.get()),
() => (document.getElementById('out-product').innerText = Prod.get()),
]);

document.getElementById('destroy-btn').addEventListener('click', () => {
Effect.destroy();
});
document.getElementById('restore-btn').addEventListener('click', () => {
Effect.restore();
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion demos/count.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createAtom, createEffect, createSelector } from '../src/api';
import { createAtom, createEffect, createSelector } from '../dist/api';

const Count = createAtom({
default: 0,
Expand Down
2 changes: 1 addition & 1 deletion demos/groups.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createGroup, createAtom, createSelector } from '../src/api';
import { createGroup, createAtom, createSelector } from '../dist/api';

const CountGroup = createGroup({
getDefault: () =>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"types": "dist/api.d.ts",
"scripts": {
"prepublishOnly": "npm run build",
"test": "mocha --recursive -r ts-node/register 'tests/**/*.test.ts' ",
"test": "mocha",
"build": "npm run build:node && npm run build:web",
"build:node": "rimraf ./dist && tsc",
"build:web": "rimraf ./cdn && rollup --config",
Expand Down
23 changes: 2 additions & 21 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,12 @@ export { createSelector } from './primitives/createSelector';
export { createEffect } from './primitives/createEffect';

export { createGroup } from './composites/createGroup';
export { createEffectGroup } from './composites/createEffectGroup';
export { createResource } from './composites/createResource';

export { visualizeDependencyGraph } from './utils/visualizeDependencyGraph';

export { CleanupStrategy } from './enums/CleanupStrategy';

export type { Atom } from './types/Atom';
export type { Selector } from './types/Selector';
export type { Resource } from './types/Resource';

import type { ImplementsKey as _ImplementsKey } from './types/traits/ImplementsKey';
import type { ImplementsGet as _ImplementsGet } from './types/traits/ImplementsGet';
import type { ImplementsSet as _ImplementsSet } from './types/traits/ImplementsSet';
import type { ImplementsSubscribe as _ImplementsSubscribe } from './types/traits/ImplementsSubscribe';
import type { ImplementsInvalidate as _ImplementsInvalidate } from './types/traits/ImplementsInvalidate';
import type { ImplementsRemove as _ImplementsRemove } from './types/traits/ImplementsRemove';
import type { ImplementsFind as _ImplementsFind } from './types/traits/ImplementsFind';
import type { ImplementsClear as _ImplementsClear } from './types/traits/ImplementsClear';
export namespace Traits {
export type ImplementsKey = _ImplementsKey;
export type ImplementsGet<T> = _ImplementsGet<T>;
export type ImplementsSet<T> = _ImplementsSet<T>;
export type ImplementsSubscribe = _ImplementsSubscribe;
export type ImplementsInvalidate = _ImplementsInvalidate;
export type ImplementsRemove<A> = _ImplementsRemove<A>;
export type ImplementsFind<T, A> = _ImplementsFind<T, A>;
export type ImplementsClear = _ImplementsClear;
}
export type { Trait } from './types/traits';
34 changes: 34 additions & 0 deletions src/composites/createEffectGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getNextAutoKey } from '../globals';
import { createEffect } from '../primitives/createEffect';
import { Effect } from '../types/Effect';
import { EffectGroup } from '../types/EffectGroup';
import { EffectProps } from '../types/EffectProps';

export const createEffectGroup = (
effectCallbacks: EffectProps.Callback[],
config?: Partial<EffectProps.Config>
): EffectGroup => {
const key = config?.key || getNextAutoKey();

const effects: Effect[] = effectCallbacks.map((cb, index) =>
createEffect(cb, {
debounceDuration: config?.debounceDuration,
key: `${key}_effect_${index}`,
})
);

const api = {
key,
destroy: () => {
effects.forEach((effect) => {
effect.destroy();
});
},
restore: () => {
effects.forEach((effect) => {
effect.restore();
});
},
};
return api;
};
19 changes: 0 additions & 19 deletions src/enums/CleanupStrategy.ts

This file was deleted.

112 changes: 94 additions & 18 deletions src/globals.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,104 @@
import type {
ReactiveContext,
SubscribeFunction,
} from './types/ReactiveContext';
import type { AtomMemory } from './types/AtomMemory';
import type { EffectMemory } from './types/EffectMemory';
import type { SelectorMemory } from './types/SelectorMemory';
import { MemoryBase } from './types/MemoryBase';
import { SelectorMemory } from './types/SelectorMemory';
import { AtomMemory } from './types/AtomMemory';
import { EffectMemory } from './types/EffectMemory';

export const globalMemory: Record<
string,
AtomMemory | SelectorMemory | EffectMemory
> = {};
const globalMemory = new Map<string, WeakRef<MemoryBase>>();

const rootContext: ReactiveContext = {
registerDependency: () => {},
const globalMemoryCleanupRegistry = new FinalizationRegistry<string>((key) => {
globalMemory.delete(key);
});

export const getMemoryOrDefault = <V extends MemoryBase>(
key: string,
getDefaultMemory: () => V
): V => {
const maybeMemory = globalMemory.get(key)?.deref();

if (maybeMemory) {
return maybeMemory as V;
}

const memory = getDefaultMemory();
setMemory(key, memory);
return memory;
};

export const setMemory = <V extends MemoryBase>(key: string, value: V) => {
globalMemoryCleanupRegistry.register(value, key);
globalMemory.set(key, new WeakRef(value));
};

export const deleteMemory = (key: string) => {
globalMemory.delete(key);
};

const getMemory = <T extends MemoryBase>(key: string) => {
return globalMemory.get(key)?.deref() as T | undefined;
};

export const notifyLivingSubscribers = (key: string) => {
const mem = getMemory<AtomMemory | SelectorMemory>(key);
if (!mem) return;
if (!('subscribers' in mem)) return;

for (const subKey of [...mem.subscribers.keys()]) {
const subMem = getMemory<AtomMemory | SelectorMemory | EffectMemory>(
subKey
);
if (!subMem) continue;
if ('isDirty' in subMem) {
subMem.isDirty = true;
}
if ('onDependencyChange' in subMem && subMem.onDependencyChange) {
subMem.onDependencyChange();
}
if ('subscribers' in subMem) {
notifyLivingSubscribers(subKey);
}
}
};

export const subscribeTo = (key: string, depKey: string) => {
if (key === depKey) return;

const mem = getMemory<AtomMemory | SelectorMemory>(key);
if (!mem) return;
if (!('subscribers' in mem)) return;

const depMem = getMemory<EffectMemory | SelectorMemory>(depKey);
if (!depMem) return;
if (!('dependencies' in depMem)) return;

mem.subscribers.add(depKey);
depMem.dependencies.add(key);
};
const contextStack: ReactiveContext[] = [rootContext];

export const registerDependency = (subscribe: SubscribeFunction) =>
contextStack[contextStack.length - 1].registerDependency(subscribe);
export const unsubscribeAllDependencies = (key: string) => {
const mem = getMemory<EffectMemory | SelectorMemory>(key);
if (!mem) return;
if (!('dependencies' in mem)) return;

export const pushReactiveContext = (context: ReactiveContext) =>
contextStack.push(context);
for (let depKey of mem.dependencies.values()) {
const depMem = getMemory<AtomMemory | SelectorMemory>(depKey);
if (!depMem) return;
if (!('subscribers' in depMem)) return;
depMem.subscribers.delete(key);
}
mem.dependencies.clear();
};

export const getAllLivingMemory = <T extends MemoryBase>(): T[] => {
return [...globalMemory.values()]
.map((ref) => ref.deref())
.filter((maybeVal) => maybeVal !== undefined) as T[];
};

const contextStack: string[] = [];
export const pushReactiveContext = (key: string) => contextStack.push(key);
export const popReactiveContext = () => contextStack.pop();
export const registerDependency = (key: string) =>
subscribeTo(key, contextStack[contextStack.length - 1]);

export const getNextAutoKey = (
(nextAutoKey = 1) =>
Expand Down
Loading

0 comments on commit edd4ede

Please sign in to comment.