@pistonite/pure 0.26.7 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pistonite/pure",
3
- "version": "0.26.7",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "description": "Pure TypeScript libraries for my projects",
6
6
  "homepage": "https://github.com/Pistonite/pure",
@@ -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
- /** See {@link makeErcType} */
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,
@@ -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
@@ -10,3 +10,4 @@
10
10
  export * from "./dark.ts";
11
11
  export * from "./inject_style.ts";
12
12
  export * from "./locale.ts";
13
+ export * from "./device.ts";
@@ -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
@@ -14,8 +14,10 @@ export { latest, type LatestConstructor, type UpdateArgsFn } from "./latest.ts";
14
14
  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
+ export { makePromise, type PromiseHandle } from "./util.ts";
18
+ export { scopedCapture, scopedCaptureSync } from "./capture.ts";
17
19
 
18
- // types
20
+ // helper types
19
21
  export type { AnyFn, AwaitRet } from "./util.ts";
20
22
 
21
23
  // unstable
@@ -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
+ // }
package/src/sync/util.ts CHANGED
@@ -1,23 +1,28 @@
1
- export const makePromise = <T>(): {
2
- promise: Promise<T>;
3
- resolve: (value: T | PromiseLike<T>) => void;
4
- reject: (reason?: unknown) => void;
5
- } => {
6
- let resolve;
7
- let reject;
1
+ /**
2
+ * Make a {@link PromiseHandle} with the promise object separate from
3
+ * its resolve and reject methods
4
+ */
5
+ export const makePromise = <T>(): PromiseHandle<T> => {
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ let resolve: any;
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ let reject: any;
8
10
  const promise = new Promise<T>((res, rej) => {
9
11
  resolve = res;
10
12
  reject = rej;
11
13
  });
12
- if (!resolve || !reject) {
13
- throw new Error(
14
- "Promise callbacks not set. This is a bug in the JS engine!",
15
- );
16
- }
17
14
  return { promise, resolve, reject };
18
15
  };
19
16
 
20
- export type PromiseHandle<T> = ReturnType<typeof makePromise<T>>;
17
+ /**
18
+ * A handle of the promise that breaks down the promise object
19
+ * and its resolve and reject functions
20
+ */
21
+ export type PromiseHandle<T> = {
22
+ promise: Promise<T>;
23
+ resolve: (value: T | PromiseLike<T>) => void;
24
+ reject: (reason?: unknown) => void;
25
+ };
21
26
 
22
27
  /** Shorthand for Awaited<ReturnType<T>> */
23
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any