Skip to content

Commit

Permalink
Merge pull request #5 from nihil-pro/synchronous-transactions
Browse files Browse the repository at this point in the history
Synchronous transactions
  • Loading branch information
nihil-pro authored Dec 15, 2024
2 parents 05d4bb2 + 467b958 commit 4a487bf
Show file tree
Hide file tree
Showing 12 changed files with 745 additions and 564 deletions.
6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"name": "kr-observable",
"version": "1.0.16",
"version": "1.0.22",
"description": "A proxy-based observable with a hoc for react/preact",
"module": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"scripts": {
"build": "rm -rf dist && tsc",
"test": "node --test --import tsx --experimental-strip-types"
Expand Down
19 changes: 16 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Observable
Proxy based state-manager
1. Easy to use, see examples below
2. Supports classes and plain objects
A proxy based state manager & reactive programming library
1. Easy to use and great DX. See examples below;
2. Supports classes and plain objects;
3. Supports subclassing
4. Tiny, no dependencies
5. Framework-agnostic. <br />
Expand Down Expand Up @@ -77,6 +77,7 @@ const Component = observer(function component() {
```
More complicated example on [CodeSandbox](https://codesandbox.io/p/sandbox/v7zf47)


## Api reference

### observer
Expand Down Expand Up @@ -170,6 +171,18 @@ example.plain.foo = '' // foo was changed, new value = ''
example.plain.nestedArray.push(42) // nestedArray was changed, new value = 42
```

### Ignore properties
The static `ignore` property allows you to exclude some properties
```typescript
import { Observable } from 'kr-observable';

class Foo extends Observable {
static ignore = ['foo']
foo = 1 // won't be observable
bar = 2
}
```

### makeObservable
Has the same API as Observable, but works only with plain objects
```typescript
Expand Down
109 changes: 79 additions & 30 deletions src/Observable.administration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,94 @@ import { SubscribersNotifier } from './Subscribers.notifier.js';
import { Listener, Subscriber } from './types.js';

export class ObservableAdministration {
#subscribers: Map<Subscriber, Set<string | symbol>> = new Map();
#listeners: Set<Listener> = new Set();
#changes: Set<string | symbol> = new Set();
#reportable = false
state = 0
ignore = Object.create(null);
subscribers: Map<Subscriber, Set<string | symbol>> = new Map;
listeners: Set<Listener> | undefined;
changes: Set<string | symbol> = new Set();
reportable = false
notified: Set<Subscriber> = new Set;
subscribe = (subscriber: Subscriber, keys: Set<string | symbol>) => {
if (this.#subscribers.size < this.#subscribers.set(subscriber, keys).size) {
this.#reportable = true
if (this.subscribers.size < this.subscribers.set(subscriber, keys).size) {
this.reportable = true
}
};
unsubscribe = (subscriber: Subscriber) => {
this.#subscribers.delete(subscriber)
if (this.#listeners.size === 0 && this.#subscribers.size === 0) { this.#reportable = false}
};
listen = (listener: Listener) => {
if (this.#listeners.size < this.#listeners.add(listener).size) {
this.#reportable = true
if (!this.listeners) {
this.listeners = new Set<Listener>()
this.reportable = true
}
this.listeners.add(listener)
};
unsubscribe = (subscriber: Subscriber) => {
this.subscribers.delete(subscriber)
if (this.subscribers.size === 0) {
if (this.listeners?.size === 0) {
this.reportable = false
}
}
};
unlisten = (listener: Listener) => {
this.#listeners.delete(listener)
if (this.#listeners.size === 0 && this.#subscribers.size === 0) { this.#reportable = false}
this.listeners.delete(listener)
if (this.listeners.size === 0) {
this.listeners = undefined
if (this.subscribers?.size === 0) {
this.reportable = false
}
}
};
report = (property: string | symbol, value: any) => {
if (!this.#reportable) { return }
this.#listeners.forEach(cb => cb(property, value));
if (this.#changes.size < this.#changes.add(property).size) {
this.#notify();
batch = () => {
// toDo
// testing new strategy
if (this.state === 1) {
this.state = 0
this.notify()
}
}
report = (property: string | symbol, value: any) => {
if (!this.reportable) { return }
this.listeners?.forEach(cb => cb(property, value));
this.changes.add(property)
};
#notify() {
const notified: Set<Subscriber> = new Set();
this.#changes.forEach(change => {
this.#subscribers.forEach((keys, cb) => {
if (keys.has(change) && !notified.has(cb)) {
SubscribersNotifier.notify(cb, this.#changes);
notified.add(cb);
skipped = false
notify() {
if (this.changes.size === 0) { return; }
const changes = new Set(this.changes)
this.changes.clear()
this.subscribers.forEach((keys, cb) => {
let isSubscribed = false
for (const k of keys) {
if (changes.has(k)) {
isSubscribed = true;
break
}
});
this.#changes.delete(change);
});
}
if (isSubscribed && !this.notified.has(cb)) {
const s = this.changes.size
SubscribersNotifier.notify(cb, new Set(changes));
if (this.changes.size === s) {
this.notified.add(cb)
} else {
this.skipped = true
this.notify()
}
}
})
queueMicrotask(() => {
if (!this.skipped) {
this.notified.clear()
this.changes.clear()
this.skipped = false
}
})
}
}
}

const trap = Object.create(null)
trap.report = 1
trap.subscribe = 1
trap.unsubscribe = 1
trap.listen = 1
trap.unlisten = 1
Object.freeze(trap)
export const AdmTrap = trap
48 changes: 48 additions & 0 deletions src/Observable.computed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ObservableAdministration } from './Observable.administration.js';
import { ObservableTransactions } from './Observable.transaction.js';

export class ObservableComputed {
#property: string | symbol
#descriptor: PropertyDescriptor
#adm: ObservableAdministration
#proxy: object
enumerable: boolean | undefined
configurable: boolean | undefined
#uncalled = true
#value: any
constructor(
property: string | symbol,
descriptor: PropertyDescriptor,
adm: ObservableAdministration,
proxy: object
) {
this.#property = property
this.#descriptor = descriptor
this.#adm = adm
this.#proxy = proxy
this.configurable = descriptor.configurable
this.enumerable = descriptor.enumerable
}

get = () => {
this.#adm.batch()
if (this.#uncalled) {
const result = ObservableTransactions.transaction(
() => this.#descriptor.get?.call(this.#proxy),
() => {
const prev = this.#value
this.#value = this.#descriptor.get?.call(this.#proxy)
if (prev !== this.#value) {
this.#adm.report(this.#property, this.#value)
this.#adm.state = 1
this.#adm.batch()
}
}
)
this.#value = result.result
this.#uncalled = false
return this.#value
}
return this.#value
}
}
3 changes: 3 additions & 0 deletions src/Observable.map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class ObservableMap<K,V> extends Map<K,V> {
} finally {
this.#adm.report(`${this.#key.toString()}.${key.toString()}`, value)
this.#adm.report(this.#key, value)
this.#adm.batch()
}
}

Expand All @@ -24,6 +25,7 @@ export class ObservableMap<K,V> extends Map<K,V> {
} finally {
this.#adm.report(`${this.#key.toString()}.${key.toString()}`, undefined)
this.#adm.report(this.#key, undefined)
this.#adm.batch()
}
}

Expand All @@ -32,6 +34,7 @@ export class ObservableMap<K,V> extends Map<K,V> {
return super.clear()
} finally {
this.#adm.report(this.#key, undefined)
this.#adm.batch()
}
}
}
3 changes: 3 additions & 0 deletions src/Observable.set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class ObservableSet<T> extends Set<T> {
return super.add(value)
} finally {
this.#adm.report(this.#key, value)
this.#adm.batch()
}
}

Expand All @@ -22,6 +23,7 @@ export class ObservableSet<T> extends Set<T> {
return super.delete(value)
} finally {
this.#adm.report(this.#key, undefined)
this.#adm.batch()
}
}

Expand All @@ -30,6 +32,7 @@ export class ObservableSet<T> extends Set<T> {
return super.clear()
} finally {
this.#adm.report(this.#key, undefined)
this.#adm.batch()
}
}
}
Loading

0 comments on commit 4a487bf

Please sign in to comment.