Skip to content

Commit

Permalink
add IterableWeakSet/IterableWeakMap
Browse files Browse the repository at this point in the history
  • Loading branch information
benStre committed Nov 28, 2023
1 parent d2b6714 commit 205d103
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 9 deletions.
25 changes: 25 additions & 0 deletions docs/manual/03 Pointers.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ id.val = 35; // triggers the fetch effect again
> id = 12; // does not trigger the effect
> ```
### Clearing effects
The `effect()` function returns an object with a `dispose()` method that can be called to clear the effect.
Expand Down Expand Up @@ -220,6 +221,30 @@ function task() {
}
```

### Automatic effect disposal with weak variable bindings

Effects can also be automatically disposed by using weak value bindings.
The effect is only active as long as none of the weakly bound values is garbage collected:

```ts
const x = $$(42)

// bind x to the effect as a weak value
effect(
// effect function, weak variables are passed in via function arguments
({x}) => {
console.log("x is " + x);
},
// list of weak variable bindings:
{x}
)
```

As soon the the weakly bound value (`x` in the example above) is no longer referenced anywhere,
it is garbage colleted and the effect is removed.

Weak value bindings can be used with all *object* values, not just with pointers.

## Observing pointer changes

For more fine grained control, the `observe()` function can be used to handle pointer value updates.
Expand Down
29 changes: 26 additions & 3 deletions functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,33 @@ export function always(scriptOrJSTransform:TemplateStringsArray|SmartTransformFu
* x.val = 6; // no log
* ```
*/
export function effect(handler:() => void): {dispose: () => void, [Symbol.dispose]: () => void} {
const ptr = Pointer.createSmartTransform(handler, undefined, true, true);
export function effect<W extends Record<string, WeakKey>|undefined>(handler:W extends undefined ? () => void :(weakVariables: W) => void, weakVariables?: W): {dispose: () => void, [Symbol.dispose]: () => void} {

let ptr: Pointer;

// weak variable binding
if (weakVariables) {
const weakVariablesProxy = {};
for (const [k, v] of Object.entries(weakVariables)) {
const weakRef = new WeakRef(v);
Object.defineProperty(weakVariablesProxy, k, {get() {
const val = weakRef.deref()
if (!val) {
// dispose effect
ptr.is_persistent = false;
ptr.delete()
throw Pointer.WEAK_EFFECT_DISPOSED;
}
else return val;
}})
}
const originalHandler = handler;
handler = (() => originalHandler(weakVariablesProxy)) as any;
}

ptr = Pointer.createSmartTransform(handler as any, undefined, true, true);
ptr.is_persistent = true;

return {
[Symbol.dispose||Symbol.for("Symbol.dispose")]() {
ptr.is_persistent = false;
Expand All @@ -71,7 +95,6 @@ export function effect(handler:() => void): {dispose: () => void, [Symbol.dispos
}



/**
* A generic transform function, creates a new pointer containing the result of the callback function.
* At any point in time, the pointer is the result of the callback function.
Expand Down
2 changes: 2 additions & 0 deletions network/supranet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ export class Supranet {
}
}

if (local_cache == false) endpoint_config.temporary = true;

endpoint_config.endpoint = endpoint;
endpoint_config.keys = keys;
endpoint_config.save();
Expand Down
4 changes: 4 additions & 0 deletions runtime/endpoint_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ class EndpointConfig implements EndpointConfigData {
else if (!this.storage)
logger.warn("Cannot save endpoint config");
else {
// remove endpoint config from previous storage
if (this.storage == globalThis.localStorage) globalThis.sessionStorage.removeItem(this.storageId)
else globalThis.localStorage.removeItem(this.storageId)

localStorage.setItem(this.locationId, this.temporary ? "session" : "persistent");
this.storage.setItem(this.storageId, serialized);
}
Expand Down
9 changes: 6 additions & 3 deletions runtime/pointers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { JSTransferableFunction } from "../types/js-function.ts";
import { map } from "../functions.ts";
import { sha256 } from "../utils/sha256.ts";
import { AutoMap } from "../utils/auto_map.ts";
import { IterableWeakSet } from "../utils/iterable-weak-set.ts";
import { IterableWeakMap } from "../utils/iterable-weak-map.ts";

export type observe_handler<K=any, V extends RefLike = any> = (value:V extends RefLike<infer T> ? T : V, key?:K, type?:Ref.UPDATE_TYPE, transform?:boolean, is_child_update?:boolean)=>void|boolean
export type observe_options = {types?:Ref.UPDATE_TYPE[], ignore_transforms?:boolean, recursive?:boolean}
Expand Down Expand Up @@ -2437,12 +2439,13 @@ export class Pointer<T = any> extends Ref<T> {
else return false;
}

public static WEAK_EFFECT_DISPOSED = Symbol("WEAK_EFFECT_DISPOSED")

protected smartTransform<R>(transform:SmartTransformFunction<T&R>, persistent_datex_transform?:string, forceLive = false, ignoreReturnValue = false, options?:SmartTransformOptions): Pointer<R> {
if (persistent_datex_transform) this.setDatexTransform(persistent_datex_transform) // TODO: only workaround

const deps = new Set<Ref>();
const keyedDeps = new Map<Pointer, Set<any>>().setAutoDefault(Set);
const deps = new IterableWeakSet<Ref>();
const keyedDeps = new IterableWeakMap<Pointer, Set<any>>().setAutoDefault(Set);

let isLive = false;
let isFirst = true;
Expand Down Expand Up @@ -2492,7 +2495,7 @@ export class Pointer<T = any> extends Ref<T> {
Ref.collapseValue(val, true, true);
}
catch (e) {
throw e;
if (e !== Pointer.WEAK_EFFECT_DISPOSED) console.error(e);
}
// always cleanup capturing
finally {
Expand Down
3 changes: 2 additions & 1 deletion runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { Supranet } from "../network/supranet.ts";
import { sendDatexViaHTTPChannel } from "../network/datex-http-channel.ts";
import { setCookie } from "../utils/cookies.ts";
import { addPersistentListener, removePersistentListener } from "../utils/persistent-listeners.ts";
import { endpoint_config } from "./endpoint_config.ts";

const mime = client_type === "deno" ? (await import("https://deno.land/x/mimetypes@v1.0.0/mod.ts")).mime : null;

Expand Down Expand Up @@ -1131,7 +1132,7 @@ export class Runtime {
// update endpoint cookie
const endpointName = endpoint.toString();
// TODO: store signed endpoint validation cookie
if (client_type == "browser") setCookie("datex-endpoint", endpointName);
if (client_type == "browser") setCookie("datex-endpoint", endpointName, endpoint_config.temporary ? 0 : undefined);
}

static getActiveLocalStorageEndpoints() {
Expand Down
4 changes: 2 additions & 2 deletions utils/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ export function setCookie(name: string, value: string, expDays?: number) {

value = encodeURIComponent(value)
let expiryDate = new Date("Fri, 31 Dec 9999 21:10:10 GMT");
if (expDays) {
if (expDays !== undefined) {
expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (expDays * 24 * 60 * 60 * 1000));
}
const expires = "expires=" + expiryDate.toUTCString() + ";";
const expires = expDays == 0 ? "" : "expires=" + expiryDate.toUTCString() + ";";
document.cookie = name + "=" + value + "; " + expires + " path=/;"
}

Expand Down
88 changes: 88 additions & 0 deletions utils/iterable-weak-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* A WeakMap with iterable entries
*/
export class IterableWeakMap<K extends WeakKey, V> extends Map<any, V> {

set(key: K, value: V): this {
return super.set(new WeakRef(key), value);
}

delete(key: K): boolean {
const deleting = new Set<WeakRef<K>>()
try {
for (const keyRef of super.keys() as Iterable<WeakRef<K>>) {
const unwrappedKey = keyRef.deref()
if (unwrappedKey == undefined) {
deleting.add(keyRef);
continue;
}
if (unwrappedKey === key) {
deleting.add(keyRef)
for (const keyRef of deleting) super.delete(keyRef);
return true;
}
}
return false;
}
finally {
for (const keyRef of deleting) super.delete(keyRef);
}
}

has(key: K): boolean {
for (const unwrappedKey of this.keys()) {
if (unwrappedKey === key) return true;
}
return false;
}

*keys(): IterableIterator<K> {
const deleting = new Set<WeakRef<K>>()
try {
for (const keyRef of super.keys() as Iterable<WeakRef<K>>) {
const unwrappedKey = keyRef.deref()
if (unwrappedKey == undefined) {
deleting.add(keyRef)
continue;
}
yield unwrappedKey;
}
}
finally {
for (const keyRef of deleting) super.delete(keyRef);
}
}

get(key: K): V|undefined {
for (const [unwrappedKey, val] of this.entries()) {
if (unwrappedKey === key) return val;
}
}

*values(): IterableIterator<V> {
for (const [key, val] of this.entries()) {
yield val
}
}

*entries(): IterableIterator<[K, V]> {
const deleting = new Set<WeakRef<K>>()
try {
for (const [keyRef, val] of super.entries() as Iterable<[WeakRef<K>, V]>) {
const unwrappedKey = keyRef.deref()
if (unwrappedKey == undefined) {
deleting.add(keyRef)
continue;
}
yield [unwrappedKey, val];
}
}
finally {
for (const keyRef of deleting) super.delete(keyRef);
}
}

[Symbol.iterator]() {
return this.entries()
}
}
68 changes: 68 additions & 0 deletions utils/iterable-weak-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* A WeakSet with iterable entries
*/
export class IterableWeakSet<T extends WeakKey> extends Set {

add(value: T): this {
return super.add(new WeakRef(value));
}

delete(value: T): boolean {
const deleting = new Set<WeakRef<T>>()
try {
for (const valRef of super.values() as Iterable<WeakRef<T>>) {
const val = valRef.deref()
if (val == undefined) {
deleting.add(valRef)
continue;
}
if (val === value) {
deleting.add(valRef)
return true;
}
}
return false;
}
finally {
for (const valRef of deleting) super.delete(valRef);
}
}

has(value: T): boolean {
for (const val of this.values()) {
if (val === value) return true;
}
return false;
}

*values(): IterableIterator<T> {
const deleting = new Set<WeakRef<T>>()
try {
for (const valRef of super.values() as Iterable<WeakRef<T>>) {
const val = valRef.deref()
if (val == undefined) {
deleting.add(valRef);
continue;
}
yield val;
}
}
finally {
for (const valRef of deleting) super.delete(valRef);
}
}

keys(): IterableIterator<T> {
return this.values()
}

*entries(): IterableIterator<[T, T]> {
for (const val of this.values()) {
yield [val, val]
}
}

[Symbol.iterator]() {
return this.values()
}
}

0 comments on commit 205d103

Please sign in to comment.