Compare commits

..

18 Commits

Author SHA1 Message Date
Fabian Stamm
69143a96d0 Make it ESM only 2023-11-22 22:23:28 +01:00
Fabian Stamm
c29837d850 Fix missing unsubscribe return on subscribeCollect 2021-01-26 01:52:14 +01:00
Fabian Stamm
cb060c6399 Making esmodule more compatible with browsers 2020-11-11 21:13:26 +01:00
Fabian Stamm
d0e8a97e4c Fixing type problem on AsyncIter 2020-10-19 21:31:11 +02:00
Fabian Stamm
84ea29a210 Fix it for real now... 2020-10-19 21:14:21 +02:00
Fabian Stamm
ddd95889f5 Fixing missing readme problem 2020-10-19 19:51:48 +02:00
Fabian Stamm
0c50717a4c Adding onClose callback to AsyncIter 2020-10-19 18:37:34 +02:00
Fabian Stamm
799651d37f Adding missing exports 2020-10-18 01:12:40 +02:00
Fabian Stamm
ef423a947a Add Deno support 2020-10-15 01:48:52 +02:00
Fabian Stamm
6000e69dcd Improving ESModule support 2020-08-26 11:54:51 +02:00
Fabian Stamm
5203871aa8 Support ESModules 2020-08-26 10:58:27 +02:00
Fabian Stamm
7a649fa1b0 Adding custom check function for await store 2020-08-21 00:21:32 +02:00
Fabian Stamm
35323a095a Fixing MemoryLeak 2020-06-11 20:49:44 +02:00
Fabian Stamm
858a508190 Remove unnecessary timer from obeservable 2020-04-13 03:11:12 +02:00
Fabian Stamm
2545f3a050 Adding JSDoc comments to awaiter 2020-02-06 18:05:05 +01:00
Fabian
811b03fd1b Fixing bug where subscribers are skipped, when subscriber unsubscribes directly 2019-11-26 23:19:06 +01:00
Fabian Stamm
6720432e24 Fixing some bug 2019-11-26 22:06:27 +01:00
Fabian Stamm
0781323acb Add AwaitStore 2019-11-25 21:31:22 +01:00
15 changed files with 404 additions and 120 deletions

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
root=true
[*]
charset = utf-8
indent_size = 3
indent_style = space
insert_final_newline = true

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/ node_modules/
lib/ lib/
esm/

17
meta.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "utils",
"version": "2.2.17",
"description": "Some helpful utility classes and functions",
"author": "Fabian Stamm <dev@fabianstamm.de>",
"contributors": [],
"root": "./esm/",
"files": [
"**/*.ts",
"**/*.js",
"esm/readme.md"
],
"hooks": {
"prepublish": "pre.js"
},
"readme": "readme.md"
}

8
package-lock.json generated
View File

@ -1,13 +1,13 @@
{ {
"name": "@hibas123/utils", "name": "@hibas123/utils",
"version": "2.1.1", "version": "2.2.17",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"typescript": { "typescript": {
"version": "3.5.3", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", "resolved": "https://npm.hibas123.de/typescript/-/typescript-4.0.5.tgz",
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==",
"dev": true "dev": true
} }
} }

View File

@ -1,28 +1,31 @@
{ {
"name": "@hibas123/utils", "name": "@hibas123/utils",
"version": "2.1.1", "version": "3.0.0",
"description": "Different Utilities, that are not worth own packages", "description": "Different Utilities, that are not worth own packages",
"main": "lib/index.js", "type": "module",
"types": "lib/index.d.ts", "main": "esm/index.js",
"scripts": { "types": "esm/index.d.ts",
"prepublishOnly": "tsc", "module": "esm/index.js",
"build": "tsc", "scripts": {
"watch-ts": "tsc -w" "prepublishOnly": "npm run build",
}, "build": "tsc",
"author": "Fabian Stamm <dev@fabianstamm.de>", "watch-ts": "tsc -w",
"license": "MIT", "postpublish": "denreg publish"
"repository": { },
"url": "https://git.stamm.me/OpenServer/Utils.git", "author": "Fabian Stamm <dev@fabianstamm.de>",
"type": "git" "license": "MIT",
}, "repository": {
"devDependencies": { "url": "https://git.stamm.me/OpenServer/Utils.git",
"typescript": "^3.5.3" "type": "git"
}, },
"files": [ "devDependencies": {
"src/", "typescript": "^4.0.5"
"lib/", },
"tsconfig.json", "files": [
"readme.md" "src/",
], "esm/",
"private": false "tsconfig.json",
} "readme.md"
],
"private": false
}

15
pre.js Normal file
View File

@ -0,0 +1,15 @@
const rjson = async (file) => JSON.parse(await Deno.readTextFile(file));
const wjson = (file, data) =>
Deno.writeTextFile(file, JSON.stringify(data, undefined, 3));
const pkg = await rjson("package.json");
const meta = await rjson("meta.json");
console.log("Changing meta.version from", meta.version, "to", pkg.version);
meta.version = pkg.version || meta.version;
await wjson("meta.json", meta);
await Deno.copyFile("readme.md", "esm/readme.md");
await Deno.copyFile("esm/index.js", "esm/mod.js");
await Deno.copyFile("esm/index.d.ts", "esm/mod.d.ts");

View File

@ -35,8 +35,8 @@ Usage:
const server = new Observable(); // Get new Observable Server const server = new Observable(); // Get new Observable Server
// Server can only send, not subscribe to messages // Server can send and subscribe to messages
// Receiving is only possible via the public API // The publicAPI will only make the receiving parts available
const public = server.getPublicApi(); const public = server.getPublicApi();
const func = (data)=>{ const func = (data)=>{
@ -44,10 +44,15 @@ Usage:
} }
// func will be callen when a message is available // func will be callen when a message is available
public.subscribe(func); let unsubscribe = public.subscribe(func);
server.send("Hello World"); server.send("Hello World");
// Unsubscribe using the returned function from subscribe
unsubscribe();
// OR
// This will unsubscribe the function. Please note, that it can // This will unsubscribe the function. Please note, that it can
// only unsubscribe the exact function, that is used in subscribe // only unsubscribe the exact function, that is used in subscribe
public.unsubscribe(func); public.unsubscribe(func);
@ -56,6 +61,44 @@ Usage:
server.send("Hello World2"); server.send("Hello World2");
``` ```
## AwaitStore
This component can be used to await a specific value or to act as an variable with events.
Usage:
``` typescript
import { AwaitStore } from "@hibas123/util";
const server = new AwaitStore<number>(0); // Set initial value
// Server can send and subscribe to messages
// Only receiving is possible using the public API
const public = server.getPublicApi();
const func = (data)=>{
console.log(data);
}
// awaitValue will also return a .ignore() function, that can be used to abort the promise completely
public.awaitValue(5).then(()=>console.log("Got 5"));
// func will be callen when a message is available and once with the current value
// This will call func with the current value (0) and on every change after.
public.subscribe(func);
// This send will trigger the subscribtion func and also release the awaitValue causing "Got 5"
// to appear in the console
server.send(5);
// This will unsubscribe the function. Please note, that it can
// only unsubscribe the exact function, that is used in subscribe
public.unsubscribe(func);
// This now won't call func anymore
server.send(8);
```
## License ## License
MIT MIT

77
src/asynciter.ts Normal file
View File

@ -0,0 +1,77 @@
import Observable from "./observable.js";
import Signal from "./signal.js";
interface IAsyncIteratorMessage<T> {
error: Error | undefined;
data: T | undefined;
close: true | undefined;
}
export default class AsyncIteratorFromCB<T> implements AsyncIterable<T> {
#onClose: (() => void)[] = [];
#onData = new Observable<IAsyncIteratorMessage<T>>();
constructor() {}
public onClose(callback: () => void) {
this.#onClose.push(callback);
}
public getCallback() {
return (error: Error | undefined, data: T | undefined) => {
this.#onData.send({ error, data, close: undefined });
};
}
public send(data: T) {
this.#onData.send({
error: undefined,
data,
close: undefined,
});
}
public close() {
this.#onData.send({
close: true,
data: undefined,
error: undefined,
});
this.#onData.close();
this.#onClose.forEach((cb) => cb());
}
[Symbol.asyncIterator](): AsyncIterator<T, undefined, any> {
const queue: IAsyncIteratorMessage<T>[] = [];
const signal = new Signal();
this.#onData.subscribe((data) => {
queue.push(data);
signal.sendSignal();
});
return {
async next() {
const send = () => {
const value = queue.shift();
if (!value) throw new Error("Error in AsyncIter");
if (value.close) {
return {
done: true,
value: undefined as any,
};
} else if (value.error) {
throw value.error;
} else {
return {
done: false,
value: value.data as T,
};
}
};
if (queue.length > 0) {
return send();
} else {
await signal.awaitSignal();
return send();
}
},
};
}
}

105
src/awaiter.ts Normal file
View File

@ -0,0 +1,105 @@
import Observable, { ObserverCallback } from "./observable.js";
export type CheckFunction<T> = (val: T) => boolean;
export default class AwaitStore<T = any> {
private observable = new Observable<T>();
constructor(private _value: T) {
this.subscribe = this.subscribe.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
}
/**
* Get the current value
*/
get value() {
return this._value;
}
/**
* Set a new value and notify subscribers
* @param value Value to be set
*/
send(value: T) {
this._value = value;
this.observable.send(value);
}
/**
* Get the current value as well as all changes
* @param handler Handler called on change
*/
subscribe(handler: ObserverCallback<T>) {
handler(this._value);
return this.observable.subscribe(handler);
}
/**
* Unsubscribe from changes
* @param handler The handler to unsubscribe
*/
unsubscribe(handler: ObserverCallback<T>) {
this.observable.unsubscribe(handler);
}
/**
* Await a specific value and return.
*
* For example if val = true then this function would block until the value
* is actually true. If it is true, then the promise will resolve immediatly
*
* @param val Value to await
*/
awaitValue(
val: T | CheckFunction<T>
): PromiseLike<void> & {
catch: (cb: (err: any) => PromiseLike<void>) => PromiseLike<void>;
ignore: () => void;
} {
let ignore: () => void = () => undefined;
let prms = new Promise<void>((yes) => {
const cb = () => {
if (typeof val === "function") {
if ((val as CheckFunction<T>)(this._value)) {
yes();
this.unsubscribe(cb);
}
} else if (this._value === val) {
yes();
this.unsubscribe(cb);
}
};
this.subscribe(cb);
});
return {
then: prms.then.bind(prms),
catch: prms.catch.bind(prms),
ignore: () => ignore(),
};
}
/**
* Creates Public API with subscribe and unsubscribe
*
* @returns {object}
*/
getPublicApi() {
if (this.observable.closed) throw new Error("Observable is closed!");
return {
subscribe: (callback: ObserverCallback<T>) => this.subscribe(callback),
unsubscribe: (callback: ObserverCallback<T>) =>
this.unsubscribe(callback),
awaitValue: (value: T) => this.awaitValue(value),
};
}
/**
* Close this store. All subscribers will be unsubscribed and any further operations will fail
*/
close() {
this.observable.close();
}
}

View File

@ -1,11 +1,22 @@
import Lock, { Release } from "./lock"; import Lock, { Release } from "./lock.js";
import Observable, { ObserverCallback, ObserverCallbackCollect, ObservableInterface } from "./observable";
export { import Observable, {
Lock, ObserverCallback,
Release, ObserverCallbackCollect,
Observable, ObservableInterface,
ObserverCallback, } from "./observable.js";
ObserverCallbackCollect,
ObservableInterface import AwaitStore from "./awaiter.js";
}
import AsyncIteratorFromCB from "./asynciter.js";
import Signal from "./signal.js";
export type {
Release,
ObserverCallback,
ObserverCallbackCollect,
ObservableInterface,
};
export { Lock, Observable, AwaitStore, AsyncIteratorFromCB, Signal };

View File

@ -2,14 +2,14 @@ export type Release = { release: () => void };
/** /**
* Basic Locking mechanism for JavaScript * Basic Locking mechanism for JavaScript
* *
*/ */
export default class Lock { export default class Lock {
private _locked: boolean = false; private _locked: boolean = false;
/** /**
* Returns the state of the Locken * Returns the state of the Locken
* *
* @returns {boolean} * @returns {boolean}
*/ */
get locked(): boolean { get locked(): boolean {
@ -23,7 +23,7 @@ export default class Lock {
/** /**
* Waits till lock is free and returns a release function * Waits till lock is free and returns a release function
* *
* @return {function} * @return {function}
*/ */
async getLock(): Promise<Release> { async getLock(): Promise<Release> {
@ -32,8 +32,8 @@ export default class Lock {
return new Promise<Release>((resolve) => { return new Promise<Release>((resolve) => {
this.toCome.push(() => { this.toCome.push(() => {
resolve({ release: this.lock() }); resolve({ release: this.lock() });
}) });
}) });
} }
} }

View File

@ -8,9 +8,9 @@ export type ObservableInterface<T> = {
*/ */
subscribe: (callback: ObserverCallback<T>) => void; subscribe: (callback: ObserverCallback<T>) => void;
/** /**
* Unsubscribe fron Observable * Unsubscribe fron Observable
* @param {function} callback * @param {function} callback
*/ */
unsubscribe: (callback: ObserverCallback<T>) => void; unsubscribe: (callback: ObserverCallback<T>) => void;
/** /**
* Subscribe to Observable in collect mode * Subscribe to Observable in collect mode
@ -20,7 +20,6 @@ export type ObservableInterface<T> = {
}; };
const ClosedSymbol = Symbol("Observable Closed"); const ClosedSymbol = Symbol("Observable Closed");
const LastValueSymbol = Symbol("Observable LastValue");
export default class Observable<T = any> { export default class Observable<T = any> {
private subscriber: ObserverCallback<T>[] = []; private subscriber: ObserverCallback<T>[] = [];
@ -31,75 +30,57 @@ export default class Observable<T = any> {
// Use symbol to make sure this property cannot be changed from the outside // Use symbol to make sure this property cannot be changed from the outside
private [ClosedSymbol] = false; private [ClosedSymbol] = false;
private [LastValueSymbol]: T | undefined = undefined;
get lastValue() {
return this[LastValueSymbol];
}
get closed() { get closed() {
return this[ClosedSymbol]; return this[ClosedSymbol];
} }
constructor(private collect_intervall: number = 100) { } constructor(private collect_intervall: number = 100) {}
/**
* Subscribe to changes
* @param callback Callback called once a value is available
*/
subscribe(callback: ObserverCallback<T>) { subscribe(callback: ObserverCallback<T>) {
if (this[ClosedSymbol]) if (this[ClosedSymbol]) throw new Error("Observable is closed!");
throw new Error("Observable is closed!");
let oldcb = this.subscriber.find(e => e === callback); let oldcb = this.subscriber.find((e) => e === callback);
if (!oldcb) if (!oldcb) this.subscriber.push(callback);
this.subscriber.push(callback)
return () => this.unsubscribe(callback);
} }
/**
* Unsubscribe specified callback. After that, it will be discarded.
* @param callback The callback originally subscribed
*/
unsubscribe(callback: ObserverCallback<T> | ObserverCallbackCollect<T>) { unsubscribe(callback: ObserverCallback<T> | ObserverCallbackCollect<T>) {
if (this[ClosedSymbol]) if (this[ClosedSymbol]) return;
return;
let idx = this.subscriber.findIndex(e => e === callback); let idx = this.subscriber.findIndex((e) => e === callback);
if (idx >= 0) { if (idx >= 0) {
this.subscriber.splice(idx, 1); this.subscriber.splice(idx, 1);
} else { } else {
idx = this.subscriberCollect.findIndex(e => e === callback); idx = this.subscriberCollect.findIndex((e) => e === callback);
if (idx >= 0) if (idx >= 0) this.subscriberCollect.splice(idx, 1);
this.subscriberCollect.splice(idx, 1);
} }
} }
/**
* Subscribe for a collection of changes
* @param callback Callback called once a or some values are available.
*/
subscribeCollect(callback: ObserverCallbackCollect<T>) { subscribeCollect(callback: ObserverCallbackCollect<T>) {
if (this[ClosedSymbol]) if (this[ClosedSymbol]) throw new Error("Observable is closed!");
throw new Error("Observable is closed!");
let oldcb = this.subscriberCollect.find(e => e === callback); let oldcb = this.subscriberCollect.find((e) => e === callback);
if (!oldcb) if (!oldcb) this.subscriberCollect.push(callback);
this.subscriberCollect.push(callback)
return () => this.unsubscribe(callback);
} }
/** /**
* Creates Public API with subscribe and unsubscribe * Creates Public API with subscribe and unsubscribe
* *
* @returns {object} * @returns {object}
*/ */
getPublicApi(): ObservableInterface<T> { getPublicApi(): ObservableInterface<T> {
if (this[ClosedSymbol]) if (this[ClosedSymbol]) throw new Error("Observable is closed!");
throw new Error("Observable is closed!");
return { return {
subscribe: (callback: ObserverCallback<T>) => this.subscribe(callback), subscribe: (callback: ObserverCallback<T>) => this.subscribe(callback),
unsubscribe: (callback: ObserverCallback<T> | ObserverCallbackCollect<T>) => this.unsubscribe(callback), unsubscribe: (
subscribeCollect: (callback: ObserverCallbackCollect<T>) => this.subscribeCollect(callback) callback: ObserverCallback<T> | ObserverCallbackCollect<T>
} ) => this.unsubscribe(callback),
subscribeCollect: (callback: ObserverCallbackCollect<T>) =>
this.subscribeCollect(callback),
};
} }
/** /**
@ -107,20 +88,27 @@ export default class Observable<T = any> {
* @param data data to be sent * @param data data to be sent
*/ */
send(data: T) { send(data: T) {
if (this[ClosedSymbol]) if (this[ClosedSymbol]) throw new Error("Observable is closed!");
throw new Error("Observable is closed!");
Array.from(this.subscriber.values()).forEach((e) => {
this[LastValueSymbol] = data; try {
this.subscriber.forEach(e => e(data)); e(data);
this.events.push(data); } catch (err) {
if (!this.timeout) { // Catch error, so it doesn't affect other subscribers
this.timeout = setTimeout(() => { console.error(err);
this.subscriberCollect.forEach(cb => { }
cb(this.events) });
}); if (this.subscribeCollect.length > 0) {
this.events = []; this.events.push(data);
this.timeout = undefined; if (!this.timeout) {
}, this.collect_intervall); this.timeout = setTimeout(() => {
this.subscriberCollect.forEach((cb) => {
cb(this.events);
});
this.events = [];
this.timeout = undefined;
}, this.collect_intervall);
}
} }
} }
@ -133,7 +121,6 @@ export default class Observable<T = any> {
this.subscriber = []; this.subscriber = [];
this.subscriberCollect = []; this.subscriberCollect = [];
this.events = []; this.events = [];
if (this.timeout) if (this.timeout) clearTimeout(this.timeout);
clearTimeout(this.timeout)
} }
} }

12
src/signal.ts Normal file
View File

@ -0,0 +1,12 @@
export default class Signal {
awaiter: (() => void)[] = [];
sendSignal(): void {
this.awaiter.forEach((a) => a());
this.awaiter = [];
}
awaitSignal(): Promise<void> {
return new Promise((resolve) => {
this.awaiter.push(resolve);
});
}
}

View File

@ -1,15 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2015", "target": "esnext",
"module": "commonjs", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "Node",
"outDir": "lib", "outDir": "esm",
"preserveWatchOutput": true, "preserveWatchOutput": true,
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
"isolatedModules": true
}, },
"include": [ "include": ["./src"]
"./src" }
]
}

8
yarn.lock Normal file
View File

@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
typescript@^4.0.5:
version "4.9.5"
resolved "https://npm.hibas123.de/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==