@pistonite/pure 0.26.8 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/memory/async_erc.ts +13 -1
- package/src/memory/emp.ts +88 -0
- package/src/memory/erc.ts +9 -0
- package/src/memory/index.ts +2 -1
- package/src/pref/device.ts +127 -0
- package/src/pref/index.ts +1 -0
- package/src/sync/RwLock.ts +2 -0
- package/src/sync/capture.ts +36 -0
- package/src/sync/index.ts +1 -0
- package/src/sync/mutex.ts +21 -0
package/package.json
CHANGED
package/src/memory/async_erc.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* operations are asynchronous.
|
|
4
4
|
*
|
|
5
5
|
* See {@link makeErcType} for how to use
|
|
6
|
+
*
|
|
7
|
+
* @deprecated use Emp
|
|
6
8
|
*/
|
|
7
9
|
export type AsyncErc<TName, TRepr = number> = {
|
|
8
10
|
readonly type: TName;
|
|
@@ -55,6 +57,8 @@ export type AsyncErc<TName, TRepr = number> = {
|
|
|
55
57
|
* Weak reference to an externally ref-counted object.
|
|
56
58
|
*
|
|
57
59
|
* See {@link makeErcType} for how to use
|
|
60
|
+
*
|
|
61
|
+
* @deprecated use Emp
|
|
58
62
|
*/
|
|
59
63
|
export type AsyncErcRef<TName, TRepr = number> = {
|
|
60
64
|
readonly type: TName;
|
|
@@ -74,11 +78,17 @@ export type AsyncErcRef<TName, TRepr = number> = {
|
|
|
74
78
|
getStrong: () => Promise<AsyncErc<TName, TRepr>>;
|
|
75
79
|
};
|
|
76
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @deprecated use Emp
|
|
83
|
+
*/
|
|
77
84
|
export type AsyncErcRefType<T> =
|
|
78
85
|
T extends AsyncErc<infer TName, infer TRepr>
|
|
79
86
|
? AsyncErcRef<TName, TRepr>
|
|
80
87
|
: never;
|
|
81
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @deprecated use Emp
|
|
91
|
+
*/
|
|
82
92
|
export type AsyncErcTypeConstructor<TName, TRepr> = {
|
|
83
93
|
/**
|
|
84
94
|
* A marker value for the underlying object type.
|
|
@@ -101,7 +111,9 @@ export type AsyncErcTypeConstructor<TName, TRepr> = {
|
|
|
101
111
|
addRef: (value: TRepr) => Promise<TRepr> | TRepr;
|
|
102
112
|
};
|
|
103
113
|
|
|
104
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* @deprecated use Emp
|
|
116
|
+
*/
|
|
105
117
|
export const makeAsyncErcType = <TName, TRepr>({
|
|
106
118
|
marker,
|
|
107
119
|
free,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A non-null, **E**ngine-**m**anaged **P**ointer
|
|
3
|
+
*
|
|
4
|
+
* This uses the ECMA FinalizationRegistry, to free the resource,
|
|
5
|
+
* once this object is garbage-collected.
|
|
6
|
+
*
|
|
7
|
+
* The free function may be async.
|
|
8
|
+
*
|
|
9
|
+
* ## Use case
|
|
10
|
+
* When JS interoperates with a system language like C/C++ or Rust,
|
|
11
|
+
* it is often needed to transfer objects into JS context. If the object
|
|
12
|
+
* is big, copy-transfer could be expensive, so the more ideal solution
|
|
13
|
+
* is to transfer a "handle", or a pointer, to JS, and leaving the actual
|
|
14
|
+
* object in the native memory. Then, JS code and pass the pointer to external
|
|
15
|
+
* code to use or extract data from the object when needed.
|
|
16
|
+
*
|
|
17
|
+
* This approach is just like passing raw pointers in C, which means it is
|
|
18
|
+
* extremely succeptible to memory corruption bugs like double-free or
|
|
19
|
+
* use-after-free. Unlike C++ or Rust, JS also doesn't have destructors
|
|
20
|
+
* that run when an object leaves the scope (proposal for the `using` keyword exists,
|
|
21
|
+
* but you can actually double-free the object with `using`).
|
|
22
|
+
*
|
|
23
|
+
* C-style manual memory management might be sufficient for simple bindings,
|
|
24
|
+
* but for more complex scenarios, automatic memory management is needed,
|
|
25
|
+
* by tying the free call to the GC of a JS object.
|
|
26
|
+
*
|
|
27
|
+
* ## Recommended practice
|
|
28
|
+
* 1. The external code should transfer the ownership of the object
|
|
29
|
+
* to JS when passing it to JS. JS will then put the handle into an Emp
|
|
30
|
+
* to be automatically managed. This means the external code should now
|
|
31
|
+
* never free the object, and let JS free it instead.
|
|
32
|
+
* 2. The inner value of the Emp must only be used for calling
|
|
33
|
+
* external code, and the Emp must be kept alive for all runtimes, including
|
|
34
|
+
* those with heavy optimization that may reclaim the Emp during the call,
|
|
35
|
+
* if it's not referenced afterwards. See {@link scopedCapture}.
|
|
36
|
+
* The inner value must not dangle around outside of the Emp that owns it.
|
|
37
|
+
*
|
|
38
|
+
* ## Pointer Size
|
|
39
|
+
* In 32-bit context like WASM32, the inner value can be a `number`.
|
|
40
|
+
* In 64-bit context, `number` might be fine for some systems, but `bigint`
|
|
41
|
+
* is recommended.
|
|
42
|
+
*/
|
|
43
|
+
export type Emp<T, TRepr> = {
|
|
44
|
+
/** The type marker for T. This only marks the type for TypeScript and does not exist at runtime */
|
|
45
|
+
readonly __phantom: T;
|
|
46
|
+
/** The underlying pointer value */
|
|
47
|
+
readonly value: TRepr;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type EmpConstructor<T, TRepr> = {
|
|
51
|
+
/**
|
|
52
|
+
* The marker for the Emp type, used to distinguish between multiple types
|
|
53
|
+
*
|
|
54
|
+
* Typically, this is a unique symbol:
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const MyNativeType = Symbol("MyNativeType");
|
|
57
|
+
* export type MyNativeType = typeof MyNativeType;
|
|
58
|
+
*
|
|
59
|
+
* const makeMyNativeTypeEmp = makeEmpType({
|
|
60
|
+
* marker: MyNativeType,
|
|
61
|
+
* free: (ptr) => void freeMyNativeType(ptr)
|
|
62
|
+
* })
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Note that this is not used in runtime, but just used for type inference,
|
|
66
|
+
* so you can also skip passing it and specify the type parameter instead
|
|
67
|
+
*/
|
|
68
|
+
marker?: T;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Function to free the underlying object. Called when this Emp is garbage-collected
|
|
72
|
+
*/
|
|
73
|
+
free: (ptr: TRepr) => void | Promise<void>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a factory function for an {@link Emp} type.
|
|
78
|
+
*/
|
|
79
|
+
export const makeEmpType = <T, TRepr>({
|
|
80
|
+
free,
|
|
81
|
+
}: EmpConstructor<T, TRepr>): ((ptr: TRepr) => Emp<T, TRepr>) => {
|
|
82
|
+
const registry = new FinalizationRegistry(free);
|
|
83
|
+
return (ptr: TRepr) => {
|
|
84
|
+
const obj = Object.freeze({ value: ptr });
|
|
85
|
+
registry.register(obj, ptr);
|
|
86
|
+
return obj as Emp<T, TRepr>;
|
|
87
|
+
};
|
|
88
|
+
};
|
package/src/memory/erc.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* A holder for an externally ref-counted object.
|
|
3
3
|
*
|
|
4
4
|
* See {@link makeErcType} for how to use
|
|
5
|
+
* @deprecated use Emp
|
|
5
6
|
*/
|
|
6
7
|
export type Erc<TName, TRepr = number> = {
|
|
7
8
|
readonly type: TName;
|
|
@@ -54,6 +55,7 @@ export type Erc<TName, TRepr = number> = {
|
|
|
54
55
|
* Weak reference to an externally ref-counted object.
|
|
55
56
|
*
|
|
56
57
|
* See {@link makeErcType} for how to use
|
|
58
|
+
* @deprecated use Emp
|
|
57
59
|
*/
|
|
58
60
|
export type ErcRef<TName, TRepr = number> = {
|
|
59
61
|
readonly type: TName;
|
|
@@ -73,9 +75,15 @@ export type ErcRef<TName, TRepr = number> = {
|
|
|
73
75
|
getStrong: () => Erc<TName, TRepr>;
|
|
74
76
|
};
|
|
75
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated use Emp
|
|
80
|
+
*/
|
|
76
81
|
export type ErcRefType<T> =
|
|
77
82
|
T extends Erc<infer TName, infer TRepr> ? ErcRef<TName, TRepr> : never;
|
|
78
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated use Emp
|
|
86
|
+
*/
|
|
79
87
|
export type ErcTypeConstructor<TName, TRepr> = {
|
|
80
88
|
/**
|
|
81
89
|
* A marker value for the underlying object type.
|
|
@@ -241,6 +249,7 @@ export type ErcTypeConstructor<TName, TRepr> = {
|
|
|
241
249
|
* myFoo1.assign(myFoo1.value); // this will free the value since ref count is 0, and result in a dangling pointer
|
|
242
250
|
* ```
|
|
243
251
|
*
|
|
252
|
+
* @deprecated use Emp
|
|
244
253
|
*/
|
|
245
254
|
export const makeErcType = <TName, TRepr>({
|
|
246
255
|
marker,
|
package/src/memory/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { cell, type CellConstructor, type Cell } from "./cell.ts";
|
|
8
8
|
export { persist, type PersistConstructor, type Persist } from "./persist.ts";
|
|
9
|
-
export * from "./erc.ts";
|
|
10
9
|
export * from "./async_erc.ts";
|
|
10
|
+
export * from "./emp.ts";
|
|
11
|
+
export * from "./erc.ts";
|
|
11
12
|
export * from "./idgen.ts";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ilog } from "../log/internal.ts";
|
|
2
|
+
import { cell } from "../memory";
|
|
3
|
+
|
|
4
|
+
let cachedIsMobile: boolean | undefined = undefined;
|
|
5
|
+
|
|
6
|
+
/** Check if the current UserAgent is a mobile device */
|
|
7
|
+
export const isMobile = (): boolean => {
|
|
8
|
+
// just very primitive UA parsing for now,
|
|
9
|
+
// we don't need to take another dependency just to detect
|
|
10
|
+
// a few platforms
|
|
11
|
+
if (cachedIsMobile === undefined) {
|
|
12
|
+
const ua = navigator?.userAgent || "";
|
|
13
|
+
if (ua.match(/(Windows NT|Macintosh|)/i)) {
|
|
14
|
+
cachedIsMobile = false;
|
|
15
|
+
} else if (ua.match(/(Mobil|iPhone|Android|iPod|iPad)/)) {
|
|
16
|
+
cachedIsMobile = true;
|
|
17
|
+
} else if (ua.includes("Linux")) {
|
|
18
|
+
cachedIsMobile = false;
|
|
19
|
+
} else {
|
|
20
|
+
ilog.warn("unable to determine device type, assuming desktop");
|
|
21
|
+
cachedIsMobile = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return cachedIsMobile;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// stores current display mode
|
|
28
|
+
const displayMode = cell({ initial: "" });
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for display mode detection
|
|
32
|
+
*
|
|
33
|
+
* See {@link initDisplayMode}
|
|
34
|
+
*/
|
|
35
|
+
export type DisplayModeOptions<T extends string> = {
|
|
36
|
+
/**
|
|
37
|
+
* Set the initial value, if the platform doesn't support detecting
|
|
38
|
+
* the display mode. `detect()` will be used instead if supported
|
|
39
|
+
*/
|
|
40
|
+
initial?: T;
|
|
41
|
+
/**
|
|
42
|
+
* Function to determine the display mode based on viewport width and height,
|
|
43
|
+
* and if the device is mobile
|
|
44
|
+
*/
|
|
45
|
+
detect: (width: number, height: number, isMobile: boolean) => T;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize display mode detection for the app.
|
|
50
|
+
*
|
|
51
|
+
* The display mode may be based on the viewport dimensions
|
|
52
|
+
* and if the device is mobile. Also note that you can use {@link isMobile}
|
|
53
|
+
* without the display mode framework.
|
|
54
|
+
*
|
|
55
|
+
* The display modes are strings that should be passed as a type parameter
|
|
56
|
+
* to {@link initDisplayMode}. You can create an alias in your code for
|
|
57
|
+
* getting the typed version of {@link addDisplayModeSubscriber}, {@link getDisplayMode},
|
|
58
|
+
* and {@link useDisplayMode} from `pure-react`.
|
|
59
|
+
*
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import {
|
|
62
|
+
* addDisplayModeSubscriber as pureAddDisplayModeSubscriber,
|
|
63
|
+
* getDisplayMode as pureGetDisplayMode,
|
|
64
|
+
* } from "@pistonite/pure/pref";
|
|
65
|
+
* import { useDisplayMode as pureUseDisplayMode } from "@pistonite/pure-react";
|
|
66
|
+
*
|
|
67
|
+
* export const MyDisplayModes = ["mode1", "mode2"] as const;
|
|
68
|
+
* export type MyDisplayMode = (typeof MyDisplayModes)[number];
|
|
69
|
+
*
|
|
70
|
+
* export const addDisplayModeSubscriber = pureAddDisplayModeSubscriber<MyDisplayMode>;
|
|
71
|
+
* export const getDisplayMode = pureGetDisplayMode<MyDisplayMode>;
|
|
72
|
+
* export const useDisplayMode = pureUseDisplayMode<MyDisplayMode>;
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Note that this is not necessary in some simple use cases. For example,
|
|
76
|
+
* adjusting styles based on the viewport width can be done with CSS:
|
|
77
|
+
* ```css
|
|
78
|
+
* @media screen and (max-width: 800px) {
|
|
79
|
+
* /* styles for narrow mode * /
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Use this only if the display mode needs to be detected programmatically.
|
|
84
|
+
*/
|
|
85
|
+
export const initDisplayMode = <T extends string>(
|
|
86
|
+
options: DisplayModeOptions<T>,
|
|
87
|
+
) => {
|
|
88
|
+
const detectCallback = () => {
|
|
89
|
+
const mode = options.detect(
|
|
90
|
+
window.innerWidth,
|
|
91
|
+
window.innerHeight,
|
|
92
|
+
isMobile(),
|
|
93
|
+
);
|
|
94
|
+
displayMode.set(mode);
|
|
95
|
+
};
|
|
96
|
+
if (
|
|
97
|
+
window &&
|
|
98
|
+
window.addEventListener &&
|
|
99
|
+
window.innerWidth !== undefined &&
|
|
100
|
+
window.innerHeight !== undefined
|
|
101
|
+
) {
|
|
102
|
+
window.addEventListener("resize", detectCallback);
|
|
103
|
+
detectCallback();
|
|
104
|
+
} else if (options.initial !== undefined) {
|
|
105
|
+
displayMode.set(options.initial);
|
|
106
|
+
} else {
|
|
107
|
+
ilog.warn(
|
|
108
|
+
"display mode cannot be initialized, since detection is not supported and initial is not set",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Subscribe to display mode changes */
|
|
114
|
+
export const addDisplayModeSubscriber = <T extends string>(
|
|
115
|
+
subscriber: (mode: T) => void,
|
|
116
|
+
notifyImmediately?: boolean,
|
|
117
|
+
) => {
|
|
118
|
+
return displayMode.subscribe(
|
|
119
|
+
subscriber as (x: string) => void,
|
|
120
|
+
notifyImmediately,
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Get the current display mode */
|
|
125
|
+
export const getDisplayMode = <T extends string>(): T => {
|
|
126
|
+
return displayMode.get() as T;
|
|
127
|
+
};
|
package/src/pref/index.ts
CHANGED
package/src/sync/RwLock.ts
CHANGED
|
@@ -6,6 +6,8 @@ import Deque from "denque";
|
|
|
6
6
|
* Only guaranteed if no one else has reference to the inner object
|
|
7
7
|
*
|
|
8
8
|
* It can take a second type parameter to specify interface with write methods
|
|
9
|
+
*
|
|
10
|
+
* @deprecated unstable API
|
|
9
11
|
*/
|
|
10
12
|
export class RwLock<TRead, TWrite extends TRead = TRead> {
|
|
11
13
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const captured = new Set<unknown>();
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Execute an async closure `fn`, and guarantee that `obj` will not be
|
|
5
|
+
* garbage-collected, until the promise is resolved.
|
|
6
|
+
*/
|
|
7
|
+
export const scopedCapture = async <T>(
|
|
8
|
+
fn: () => Promise<T>,
|
|
9
|
+
obj: unknown,
|
|
10
|
+
): Promise<T> => {
|
|
11
|
+
// captures the object
|
|
12
|
+
// technically, this is not needed, as the delete() call above
|
|
13
|
+
// should make sure the captured object is not GC'ed.
|
|
14
|
+
// However, making it reachable from a global object will definitely
|
|
15
|
+
// prevent GC even with crazy optimization from any runtime
|
|
16
|
+
captured.add(obj);
|
|
17
|
+
try {
|
|
18
|
+
return await fn();
|
|
19
|
+
} finally {
|
|
20
|
+
captured.delete(obj);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute a closure `fn`, and guarantee that `obj` will not be
|
|
26
|
+
* garbage-collected during the execution
|
|
27
|
+
*/
|
|
28
|
+
export const scopedCaptureSync = <T>(fn: () => T, obj: unknown): T => {
|
|
29
|
+
// captures the object
|
|
30
|
+
captured.add(obj);
|
|
31
|
+
try {
|
|
32
|
+
return fn();
|
|
33
|
+
} finally {
|
|
34
|
+
captured.delete(obj);
|
|
35
|
+
}
|
|
36
|
+
};
|
package/src/sync/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { debounce, type DebounceConstructor } from "./debounce.ts";
|
|
|
15
15
|
export { batch, type BatchConstructor } from "./batch.ts";
|
|
16
16
|
export { once, type OnceConstructor } from "./once.ts";
|
|
17
17
|
export { makePromise, type PromiseHandle } from "./util.ts";
|
|
18
|
+
export { scopedCapture, scopedCaptureSync } from "./capture.ts";
|
|
18
19
|
|
|
19
20
|
// helper types
|
|
20
21
|
export type { AnyFn, AwaitRet } from "./util.ts";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-reentrant mutex
|
|
3
|
+
*
|
|
4
|
+
* This allows only one context to enter a block at a time in a FIFO manner.
|
|
5
|
+
*
|
|
6
|
+
* This mutex is non-reentrant. Trying to lock it again while the same
|
|
7
|
+
* context already owns the lock will cause a dead lock.
|
|
8
|
+
*
|
|
9
|
+
* While a context id can be used to implement reentrant locks,
|
|
10
|
+
* it is very cumbersome to use. https://github.com/tc39/proposal-async-context
|
|
11
|
+
* will allow for a cleaner implementation.
|
|
12
|
+
*/
|
|
13
|
+
// TODO: implement it if needed
|
|
14
|
+
// export class Mutex {
|
|
15
|
+
// private waiters: Deque;
|
|
16
|
+
//
|
|
17
|
+
// constructor() {
|
|
18
|
+
// this.waiters = new
|
|
19
|
+
// }
|
|
20
|
+
//
|
|
21
|
+
// }
|