Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implementing TC39 signal polyfill with tansu #145

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ temp
*.md
coverage
.angular
tests
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@
"main": "./dist/package/index.cjs",
"module": "./dist/package/index.js",
"exports": {
"types": "./dist/package/index.d.ts",
"require": "./dist/package/index.cjs",
"default": "./dist/package/index.js"
".": {
"types": "./dist/package/index.d.ts",
"require": "./dist/package/index.cjs",
"default": "./dist/package/index.js"
},
"./polyfill": {
"types": "./dist/package/wrapper.d.ts",
"require": "./dist/package/wrapper.cjs",
"default": "./dist/package/wrapper.js"
}
},
"license": "MIT",
"repository": {
Expand Down
20 changes: 13 additions & 7 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@ export default defineConfig({
output: [
{
format: 'cjs',
file: './dist/package/index.cjs',
dir: './dist/package',
entryFileNames: (chunk) => `${chunk.name}.cjs`,
},
{
format: 'es',
file: './dist/package/index.js',
dir: './dist/package',
entryFileNames: (chunk) => `${chunk.name}.js`,
},
],
input: './src/index.ts',
input: { index: './src/index.ts', wrapper: './src/wrapper.ts' },
plugins: [
typescript(),
typescript({
tsconfig: 'tsconfig.d.json',
}),
{
name: 'package',
async buildStart() {
Expand All @@ -36,9 +40,11 @@ export default defineConfig({
pkg.typings = removeDistPackage(pkg.typings);
pkg.main = removeDistPackage(pkg.main);
pkg.module = removeDistPackage(pkg.module);
pkg.exports.types = removeDistPackage(pkg.exports.types);
pkg.exports.require = removeDistPackage(pkg.exports.require);
pkg.exports.default = removeDistPackage(pkg.exports.default);
for (const entryPoint of Object.values(pkg.exports)) {
entryPoint.types = removeDistPackage(entryPoint.types);
entryPoint.require = removeDistPackage(entryPoint.require);
entryPoint.default = removeDistPackage(entryPoint.default);
}
this.emitFile({ type: 'asset', fileName: 'package.json', source: JSON.stringify(pkg) });
this.emitFile({
type: 'asset',
Expand Down
4 changes: 4 additions & 0 deletions src/internal/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { SignalStore, SubscribableStore } from '../types';

export interface Consumer {
markDirty(): void;
wrapper?: any;
}

export const enum RawStoreFlags {
Expand All @@ -13,6 +14,7 @@ export const enum RawStoreFlags {
// the following flags are used in RawStoreComputedOrDerived and derived classes
COMPUTING = 1 << 3,
DIRTY = 1 << 4,
COMPUTED_WITH_ONUSE = 1 << 5,
}

export interface BaseLink<T> {
Expand All @@ -27,9 +29,11 @@ export interface RawStore<T, Link extends BaseLink<T> = BaseLink<T>>
newLink(consumer: Consumer): Link;
registerConsumer(link: Link): Link;
unregisterConsumer(link: Link): void;
recCallOnUse(): boolean;
updateValue(): void;
isLinkUpToDate(link: Link): boolean;
updateLink(link: Link): T;
wrapper?: any;
}

export const updateLinkProducerValue = <T>(link: BaseLink<T>): void => {
Expand Down
28 changes: 23 additions & 5 deletions src/internal/storeComputed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class RawStoreComputed<T>
implements Consumer, ActiveConsumer
{
private producerIndex = 0;
private producerLinks: BaseLink<any>[] = [];
public producerLinks: BaseLink<any>[] = [];
private epoch = -1;

constructor(private readonly computeFn: () => T) {
Expand All @@ -33,6 +33,18 @@ export class RawStoreComputed<T>
this.epoch = epoch;
}

override recCallOnUse(): boolean {
if (super.recCallOnUse()) {
const producerLinks = this.producerLinks;
for (let i = 0, l = producerLinks.length; i < l; i++) {
const link = producerLinks[i];
const producer = link.producer;
producer.recCallOnUse();
}
}
return false;
}

override get(): T {
if (
!activeConsumer &&
Expand All @@ -59,7 +71,10 @@ export class RawStoreComputed<T>
producerLinks[producerIndex] = link;
this.producerIndex = producerIndex + 1;
updateLinkProducerValue(link);
if (producer.flags & RawStoreFlags.HAS_VISIBLE_ONUSE) {
if (
!(this.flags & RawStoreFlags.COMPUTED_WITH_ONUSE) &&
producer.flags & RawStoreFlags.HAS_VISIBLE_ONUSE
) {
this.flags |= RawStoreFlags.HAS_VISIBLE_ONUSE;
}
return producer.updateLink(link);
Expand All @@ -72,6 +87,7 @@ export class RawStoreComputed<T>
link.producer.registerConsumer(link);
}
this.flags |= RawStoreFlags.DIRTY;
super.startUse();
}

override endUse(): void {
Expand All @@ -80,6 +96,7 @@ export class RawStoreComputed<T>
const link = producerLinks[i];
link.producer.unregisterConsumer(link);
}
super.endUse();
}

override areProducersUpToDate(): boolean {
Expand All @@ -103,9 +120,10 @@ export class RawStoreComputed<T>
const prevActiveConsumer = setActiveConsumer(this);
try {
this.producerIndex = 0;
this.flags &= ~RawStoreFlags.HAS_VISIBLE_ONUSE;
const computeFn = this.computeFn;
value = computeFn();
if (!(this.flags & RawStoreFlags.COMPUTED_WITH_ONUSE)) {
this.flags &= ~RawStoreFlags.HAS_VISIBLE_ONUSE;
}
value = this.computeFn.call(this.wrapper);
this.error = null;
} catch (error) {
value = COMPUTED_ERRORED;
Expand Down
3 changes: 3 additions & 0 deletions src/internal/storeConst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export class RawStoreConst<T> implements RawStore<T, BaseLink<T>> {
}
unregisterConsumer(_link: BaseLink<T>): void {}
updateValue(): void {}
recCallOnUse(): boolean {
return false;
}
isLinkUpToDate(_link: BaseLink<T>): boolean {
return true;
}
Expand Down
14 changes: 14 additions & 0 deletions src/internal/storeDerived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ abstract class RawStoreDerived<T, S extends StoresInput>
producer.registerConsumer(producer.newLink(this))
);
this.flags |= RawStoreFlags.DIRTY;
super.startUse();
}

override endUse(): void {
Expand All @@ -63,6 +64,19 @@ abstract class RawStoreDerived<T, S extends StoresInput>
link.producer.unregisterConsumer(link);
}
}
super.endUse();
}

override recCallOnUse(): boolean {
if (super.recCallOnUse()) {
const producerLinks = this.producerLinks!;
for (let i = 0, l = producerLinks.length; i < l; i++) {
const link = producerLinks[i];
const producer = link.producer;
producer.recCallOnUse();
}
}
return false;
}

override areProducersUpToDate(): boolean {
Expand Down
2 changes: 2 additions & 0 deletions src/internal/storeSubscribable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class RawSubscribableWrapper<T> extends RawStoreTrackingUsage<T> {

override startUse(): void {
this.unsubscribe = normalizeUnsubscribe(this.subscribable.subscribe(this.subscriber));
super.startUse();
}

override endUse(): void {
Expand All @@ -37,5 +38,6 @@ export class RawSubscribableWrapper<T> extends RawStoreTrackingUsage<T> {
this.unsubscribe = null;
unsubscribe();
}
super.endUse();
}
}
29 changes: 25 additions & 4 deletions src/internal/storeTrackingUsage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ export const flushUnused = (): void => {
}
};

export abstract class RawStoreTrackingUsage<T> extends RawStoreWritable<T> {
export class RawStoreTrackingUsage<T> extends RawStoreWritable<T> {
private extraUsages = 0;
abstract startUse(): void;
abstract endUse(): void;
startUseFn?: () => void;
endUseFn?: () => void;

override updateValue(): void {
startUse(): void {
this.startUseFn?.call(this.wrapper);
}
endUse(): void {
this.endUseFn?.call(this.wrapper);
}

private callOnUse(): boolean {
const flags = this.flags;
if (!(flags & RawStoreFlags.START_USE_CALLED)) {
// Ignoring coverage for the following lines because, unless there is a bug in tansu (which would have to be fixed!)
Expand All @@ -44,7 +51,21 @@ export abstract class RawStoreTrackingUsage<T> extends RawStoreWritable<T> {
}
this.flags |= RawStoreFlags.START_USE_CALLED;
untrack(() => this.startUse());
return true;
}
return false;
}

override recCallOnUse(): boolean {
return this.callOnUse();
}

override updateValue(): void {
this.callOnUse();
}

isUsed(): boolean {
return this.extraUsages > 0 || (this.consumerLinks?.length ?? 0) > 0;
}

override checkUnused(): void {
Expand Down
2 changes: 2 additions & 0 deletions src/internal/storeWithOnUse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class RawStoreWithOnUse<T> extends RawStoreTrackingUsage<T> {

override startUse(): void {
this.cleanUpFn = normalizeUnsubscribe(this.onUseFn());
super.startUse();
}

override endUse(): void {
Expand All @@ -24,5 +25,6 @@ export class RawStoreWithOnUse<T> extends RawStoreTrackingUsage<T> {
this.cleanUpFn = null;
cleanUpFn();
}
super.endUse();
}
}
7 changes: 5 additions & 2 deletions src/internal/storeWritable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class RawStoreWritable<T> implements RawStore<T, ProducerConsumerLink<T>>
equalFn = equal<T>;
private equalCache: Record<number, boolean> | null = null;
consumerLinks: ProducerConsumerLink<T>[] = [];
wrapper?: any;

newLink(consumer: Consumer): ProducerConsumerLink<T> {
return {
Expand Down Expand Up @@ -100,10 +101,12 @@ export class RawStoreWritable<T> implements RawStore<T, ProducerConsumerLink<T>>

protected checkUnused(): void {}
updateValue(): void {}
recCallOnUse(): boolean {
return false;
}

protected equal(a: T, b: T): boolean {
const equalFn = this.equalFn;
return equalFn(a, b);
return this.equalFn.call(this.wrapper, a, b);
}

protected increaseEpoch(): void {
Expand Down
1 change: 1 addition & 0 deletions src/internal/untrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BaseLink, RawStore } from './store';

export interface ActiveConsumer {
addProducer: <T, L extends BaseLink<T>>(store: RawStore<T, L>) => T;
wrapper?: any;
}

export let activeConsumer: ActiveConsumer | null = null;
Expand Down
36 changes: 36 additions & 0 deletions src/internal/watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { BaseLink, Consumer, RawStore } from './store';

export class RawWatcher implements Consumer {
producerLinks: BaseLink<any>[] = [];
dirty = false;
wrapper?: any;

constructor(public notifyFn: () => void) {}

markDirty(): void {
if (!this.dirty) {
this.dirty = true;
this.notifyFn.call(this.wrapper);
}
}

addProducer(producer: RawStore<any>): void {
const link = producer.newLink(this);
this.producerLinks.push(link);
producer.registerConsumer(link);
link.producer.recCallOnUse();
}

removeProducer(producer: RawStore<any>): void {
const producerLinks = this.producerLinks;
const index = producerLinks.findIndex((link) => link.producer === producer);
if (index > -1) {
const link = producerLinks[index];
const lastItem = producerLinks.pop()!;
if (link !== lastItem) {
producerLinks[index] = lastItem;
}
producer.unregisterConsumer(link);
}
}
}
Loading
Loading