@gjsify/utils 0.4.0 → 0.4.3
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 +44 -41
- package/src/base64.ts +0 -78
- package/src/byte-array.ts +0 -12
- package/src/callable.ts +0 -63
- package/src/cli.ts +0 -10
- package/src/defer.ts +0 -11
- package/src/encoding.ts +0 -36
- package/src/error.ts +0 -38
- package/src/file.ts +0 -12
- package/src/fs.ts +0 -24
- package/src/gio-errors.ts +0 -156
- package/src/gio.ts +0 -67
- package/src/globals.ts +0 -13
- package/src/index.ts +0 -18
- package/src/log.spec.ts +0 -32
- package/src/main-loop.ts +0 -62
- package/src/message.ts +0 -11
- package/src/microtask.ts +0 -4
- package/src/next-tick.spec.ts +0 -116
- package/src/next-tick.ts +0 -112
- package/src/path.ts +0 -52
- package/src/structured-clone.ts +0 -402
- package/src/test.ts +0 -5
- package/tsconfig.json +0 -38
- package/tsconfig.tsbuildinfo +0 -1
package/src/main-loop.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// GLib MainLoop management for GJS — original implementation
|
|
2
|
-
// Provides an implicit event loop analogous to Node.js's built-in event loop.
|
|
3
|
-
|
|
4
|
-
import type GLib from '@girs/glib-2.0';
|
|
5
|
-
|
|
6
|
-
/** Sentinel to prevent double-start (setMainLoopHook throws if called twice). */
|
|
7
|
-
let _started = false;
|
|
8
|
-
|
|
9
|
-
/** The singleton MainLoop instance, if created. */
|
|
10
|
-
let _loop: GLib.MainLoop | null = null;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Ensure a GLib MainLoop is running for async I/O dispatch (Soup.Server,
|
|
14
|
-
* Gio.SocketService, etc.). No-op on Node.js. Idempotent.
|
|
15
|
-
*
|
|
16
|
-
* - Called automatically by `http.Server.listen()`, `net.Server.listen()`,
|
|
17
|
-
* `dgram.Socket.bind()` etc.
|
|
18
|
-
* - GTK apps should NOT call this — they use `Gtk.Application.runAsync()` instead.
|
|
19
|
-
*
|
|
20
|
-
* @returns The MainLoop instance on GJS, or `undefined` on Node.js.
|
|
21
|
-
*/
|
|
22
|
-
export function ensureMainLoop(): GLib.MainLoop | undefined {
|
|
23
|
-
const gjsImports = (globalThis as any).imports;
|
|
24
|
-
if (!gjsImports) return undefined; // Not GJS
|
|
25
|
-
if (_started) return _loop!;
|
|
26
|
-
|
|
27
|
-
const GLibModule = gjsImports.gi.GLib;
|
|
28
|
-
_loop = new GLibModule.MainLoop(null, false);
|
|
29
|
-
_started = true;
|
|
30
|
-
|
|
31
|
-
// Only call runAsync() if no mainloop is currently running on the default
|
|
32
|
-
// context. If one is already running (e.g., test runner's mainloop.run()
|
|
33
|
-
// or Gtk.Application.runAsync()), async I/O already works through the
|
|
34
|
-
// shared default context — calling runAsync() would register a
|
|
35
|
-
// setMainLoopHook whose loop.run() blocks forever after tests quit it
|
|
36
|
-
// (g_main_loop_run resets the quit flag on entry).
|
|
37
|
-
if (GLibModule.main_depth() === 0) {
|
|
38
|
-
try {
|
|
39
|
-
(_loop as any).runAsync();
|
|
40
|
-
} catch {
|
|
41
|
-
// setMainLoopHook throws if already called (e.g., Gtk.Application.runAsync()).
|
|
42
|
-
// In that case, a main loop hook is already registered — no action needed.
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return _loop;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Quit the MainLoop created by `ensureMainLoop()`. Idempotent, no-op on Node.js.
|
|
51
|
-
*
|
|
52
|
-
* Calling `quit()` on a loop that hasn't started yet pre-quits it — when the
|
|
53
|
-
* `setMainLoopHook` later fires and calls `run()`, it returns immediately.
|
|
54
|
-
* This is used by `@gjsify/unit` to prevent the loop from blocking after tests.
|
|
55
|
-
*/
|
|
56
|
-
export function quitMainLoop(): void {
|
|
57
|
-
if (_loop) {
|
|
58
|
-
_loop.quit();
|
|
59
|
-
_started = false;
|
|
60
|
-
_loop = null;
|
|
61
|
-
}
|
|
62
|
-
}
|
package/src/message.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// See https://github.com/denoland/deno_std/blob/44d05e7a8d445888d989d49eb3e59eee3055f2c5/node/_utils.ts#L21
|
|
2
|
-
export const notImplemented = (msg: string) => {
|
|
3
|
-
const message = msg ? `Not implemented: ${msg}` : "Not implemented";
|
|
4
|
-
throw new Error(message);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const warnNotImplemented = (msg) => {
|
|
8
|
-
const message = msg ? `Not implemented: ${msg}` : "Not implemented";
|
|
9
|
-
console.warn(message);
|
|
10
|
-
return message;
|
|
11
|
-
}
|
package/src/microtask.ts
DELETED
package/src/next-tick.spec.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// Tests for packages/gjs/utils/src/next-tick.ts
|
|
2
|
-
// Regression: nextTick on GJS must route through GLib.idle_add(PRIORITY_HIGH_IDLE)
|
|
3
|
-
// instead of queueMicrotask, so GTK events (PRIORITY_DEFAULT = 0) can interleave
|
|
4
|
-
// between stream operations and prevent window freezes under heavy I/O.
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, on } from '@gjsify/unit';
|
|
7
|
-
import { nextTick, __resetBurstStateForTests } from './next-tick.js';
|
|
8
|
-
|
|
9
|
-
export default async () => {
|
|
10
|
-
await describe('nextTick', async () => {
|
|
11
|
-
await it('should execute the callback', async () => {
|
|
12
|
-
let called = false;
|
|
13
|
-
await new Promise<void>(resolve => {
|
|
14
|
-
nextTick(() => { called = true; resolve(); });
|
|
15
|
-
});
|
|
16
|
-
expect(called).toBeTruthy();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
await it('should be deferred — not synchronous', async () => {
|
|
20
|
-
let ranBeforeReturn = false;
|
|
21
|
-
let scheduled = false;
|
|
22
|
-
nextTick(() => { scheduled = true; });
|
|
23
|
-
// nextTick callback must not have run before this line
|
|
24
|
-
ranBeforeReturn = scheduled;
|
|
25
|
-
// Wait for callback
|
|
26
|
-
await new Promise<void>(resolve => nextTick(resolve));
|
|
27
|
-
expect(ranBeforeReturn).toBeFalsy();
|
|
28
|
-
expect(scheduled).toBeTruthy();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
await it('should pass arguments to the callback', async () => {
|
|
32
|
-
const result = await new Promise<string>(resolve => {
|
|
33
|
-
nextTick((a: string, b: string) => resolve(a + b), 'hello', ' world');
|
|
34
|
-
});
|
|
35
|
-
expect(result).toBe('hello world');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
await it('should run callbacks in scheduling order', async () => {
|
|
39
|
-
const order: number[] = [];
|
|
40
|
-
await new Promise<void>(resolve => {
|
|
41
|
-
nextTick(() => order.push(1));
|
|
42
|
-
nextTick(() => order.push(2));
|
|
43
|
-
nextTick(() => { order.push(3); resolve(); });
|
|
44
|
-
});
|
|
45
|
-
// Additional tick so all three have fired
|
|
46
|
-
await new Promise<void>(resolve => nextTick(resolve));
|
|
47
|
-
expect(order[0]).toBe(1);
|
|
48
|
-
expect(order[1]).toBe(2);
|
|
49
|
-
expect(order[2]).toBe(3);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// GJS-specific: nextTick must use GLib.idle_add so that GLib I/O callbacks
|
|
53
|
-
// (PRIORITY_DEFAULT = 0) can fire between nextTick callbacks (PRIORITY_HIGH_IDLE = 100).
|
|
54
|
-
// We verify that a resolved Promise (microtask, highest priority) fires before
|
|
55
|
-
// a nextTick, whereas a GLib.idle_add at PRIORITY_DEFAULT fires before one at PRIORITY_HIGH_IDLE.
|
|
56
|
-
await on('Gjs', async () => {
|
|
57
|
-
await it('GJS: nextTick does not block GLib I/O callbacks (priority ordering)', async () => {
|
|
58
|
-
// A Promise.resolve microtask fires before any GLib idle (same GLib dispatch).
|
|
59
|
-
// A nextTick (GLib idle 100) must not block a higher-priority GLib source (priority 0).
|
|
60
|
-
// We test indirectly: schedule two nextTick callbacks and one via Promise.resolve().
|
|
61
|
-
// The Promise.resolve microtask runs within the current GLib dispatch (before the idle).
|
|
62
|
-
const order: string[] = [];
|
|
63
|
-
await new Promise<void>(resolve => {
|
|
64
|
-
// Schedule nextTick (GLib idle priority 100 on GJS)
|
|
65
|
-
nextTick(() => { order.push('tick'); resolve(); });
|
|
66
|
-
// Schedule a microtask (Promise.resolve runs in current dispatch, before idle)
|
|
67
|
-
Promise.resolve().then(() => order.push('microtask'));
|
|
68
|
-
});
|
|
69
|
-
// On GJS: microtask fires before GLib idle, so 'microtask' comes first
|
|
70
|
-
// On Node.js: nextTick fires before Promise.resolve microtasks by spec
|
|
71
|
-
expect(order).toContain('tick');
|
|
72
|
-
expect(order).toContain('microtask');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Burst-yield behavior. When hundreds of nextTicks fire in a tight
|
|
76
|
-
// loop (webtorrent DHT bootstrap, streamx pipe bursts, …) GLib
|
|
77
|
-
// dispatches the whole batch at PRIORITY_DEFAULT before coming back
|
|
78
|
-
// to collect GTK input events — the window appears frozen. After
|
|
79
|
-
// BURST_YIELD_THRESHOLD consecutive calls within BURST_IDLE_MS, the
|
|
80
|
-
// scheduler switches to delay=1ms timeouts, forcing a main-loop
|
|
81
|
-
// iteration between bursts so GTK events can drain. Normal,
|
|
82
|
-
// non-bursty code pays zero latency because the counter resets on
|
|
83
|
-
// any gap > BURST_IDLE_MS.
|
|
84
|
-
await it('GJS: a tight burst of 256 nextTicks still completes', async () => {
|
|
85
|
-
__resetBurstStateForTests();
|
|
86
|
-
let fired = 0;
|
|
87
|
-
const target = 256;
|
|
88
|
-
await new Promise<void>(resolve => {
|
|
89
|
-
for (let i = 0; i < target; i++) {
|
|
90
|
-
nextTick(() => {
|
|
91
|
-
fired++;
|
|
92
|
-
if (fired === target) resolve();
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
expect(fired).toBe(target);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await it('GJS: order is preserved inside and across bursts', async () => {
|
|
100
|
-
__resetBurstStateForTests();
|
|
101
|
-
const order: number[] = [];
|
|
102
|
-
const target = 128;
|
|
103
|
-
await new Promise<void>(resolve => {
|
|
104
|
-
for (let i = 0; i < target; i++) {
|
|
105
|
-
nextTick(() => {
|
|
106
|
-
order.push(i);
|
|
107
|
-
if (order.length === target) resolve();
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
// FIFO across the whole burst, including the yield points.
|
|
112
|
-
for (let i = 0; i < target; i++) expect(order[i]).toBe(i);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
};
|
package/src/next-tick.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// Microtask scheduling utility for GJS
|
|
2
|
-
// Shared by @gjsify/stream, @gjsify/web-streams, and other packages
|
|
3
|
-
// Matches Node.js process.nextTick semantics with cross-platform fallbacks
|
|
4
|
-
|
|
5
|
-
declare const queueMicrotask: ((cb: () => void) => void) | undefined;
|
|
6
|
-
|
|
7
|
-
// Burst-yield scheduler. GTK input events (move, click, scroll) are dispatched
|
|
8
|
-
// from GLib's main context at PRIORITY_DEFAULT (0). Historically every nextTick
|
|
9
|
-
// became its own GLib.timeout_add(PRIORITY_DEFAULT, 0) source — ready
|
|
10
|
-
// immediately. When user code (webtorrent DHT bootstrap, streamx pipe bursts)
|
|
11
|
-
// scheduled hundreds of nextTicks in a tight loop, GLib dispatched the whole
|
|
12
|
-
// batch before cycling back to collect GTK input events, freezing the window.
|
|
13
|
-
//
|
|
14
|
-
// Instead, we maintain a FIFO queue owned by this module:
|
|
15
|
-
// • nextTick(cb) pushes onto _queue
|
|
16
|
-
// • A single GLib.timeout_add(PRIORITY_DEFAULT, 0) drains up to CHUNK_SIZE
|
|
17
|
-
// callbacks per iteration
|
|
18
|
-
// • If more remain, the drainer re-arms with delay=1ms — forcing at least
|
|
19
|
-
// one main-loop iteration before continuing, so GTK events that arrived
|
|
20
|
-
// during the chunk get dispatched
|
|
21
|
-
// • When _queue empties, the drainer goes idle (zero ambient cost)
|
|
22
|
-
//
|
|
23
|
-
// Guarantees:
|
|
24
|
-
// • FIFO: single shared queue + single drainer preserves call order
|
|
25
|
-
// • Throughput: short bursts under CHUNK_SIZE drain with zero added latency
|
|
26
|
-
// • Responsiveness: longer bursts cost at most 1ms per CHUNK_SIZE callbacks
|
|
27
|
-
// of added latency, in exchange for GTK input dispatch
|
|
28
|
-
// • GC safety: one timeout source lives while _queue is non-empty; no
|
|
29
|
-
// per-call BoxedInstance retention
|
|
30
|
-
const CHUNK_SIZE = 64;
|
|
31
|
-
const YIELD_DELAY_MS = 1;
|
|
32
|
-
const _queue: Array<() => void> = [];
|
|
33
|
-
let _drainerArmed = false;
|
|
34
|
-
|
|
35
|
-
function drainOnce(GLib: any): void {
|
|
36
|
-
// Process up to CHUNK_SIZE callbacks. Errors don't abort the queue —
|
|
37
|
-
// Node's process.nextTick guarantees later ticks still run even if an
|
|
38
|
-
// earlier one throws (the throw is delivered asynchronously via
|
|
39
|
-
// 'uncaughtException'). We keep the same contract by catching per-cb.
|
|
40
|
-
const end = Math.min(CHUNK_SIZE, _queue.length);
|
|
41
|
-
for (let i = 0; i < end; i++) {
|
|
42
|
-
const cb = _queue.shift()!;
|
|
43
|
-
try { cb(); }
|
|
44
|
-
catch (err) {
|
|
45
|
-
// Surface as an emitted error rather than swallow. In GJS there is no
|
|
46
|
-
// 'uncaughtException'; fall back to logging on stderr via GLib.
|
|
47
|
-
try { GLib.log_default_handler('gjsify-nextTick', GLib.LogLevelFlags.LEVEL_WARNING, String((err as any)?.stack || err), null); }
|
|
48
|
-
catch { /* best-effort */ }
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (_queue.length > 0) {
|
|
52
|
-
// More work remains — re-arm with a 1 ms yield so GTK events dispatch.
|
|
53
|
-
GLib.timeout_add(GLib.PRIORITY_DEFAULT, YIELD_DELAY_MS, () => {
|
|
54
|
-
drainOnce(GLib);
|
|
55
|
-
return false;
|
|
56
|
-
});
|
|
57
|
-
} else {
|
|
58
|
-
_drainerArmed = false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// On GJS, nextTick goes through the GLib main loop instead of the JS
|
|
63
|
-
// microtask queue, so I/O events are interleaved between stream/pipe steps.
|
|
64
|
-
//
|
|
65
|
-
// PRIORITY_DEFAULT (0) is required: GJS 1.86 maintains an internal
|
|
66
|
-
// Promise/microtask-drain source at priority 0 that returns SOURCE_CONTINUE,
|
|
67
|
-
// permanently blocking any source at priority > 0 (including PRIORITY_HIGH_IDLE
|
|
68
|
-
// = 100) from ever dispatching. Using PRIORITY_DEFAULT ensures nextTick
|
|
69
|
-
// callbacks fire within the same GLib dispatch band as I/O events.
|
|
70
|
-
//
|
|
71
|
-
// We use GLib.timeout_add rather than GLib.idle_add: timeout_add returns a
|
|
72
|
-
// numeric source ID (no BoxedInstance, no GC race). GLib.idle_add has the same
|
|
73
|
-
// GC-race hazard as the old GLib.Source BoxedInstance approach fixed in
|
|
74
|
-
// @gjsify/node-globals timers.
|
|
75
|
-
function tryGLibTimeout(cb: () => void): boolean {
|
|
76
|
-
const GLib = (globalThis as any).imports?.gi?.GLib;
|
|
77
|
-
if (!GLib?.timeout_add) return false;
|
|
78
|
-
_queue.push(cb);
|
|
79
|
-
if (!_drainerArmed) {
|
|
80
|
-
_drainerArmed = true;
|
|
81
|
-
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => {
|
|
82
|
-
drainOnce(GLib);
|
|
83
|
-
return false;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** @internal Test helper: reset burst state. */
|
|
90
|
-
export function __resetBurstStateForTests(): void {
|
|
91
|
-
_queue.length = 0;
|
|
92
|
-
_drainerArmed = false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Schedule a function on the next turn of the event loop.
|
|
97
|
-
* On GJS: uses GLib.timeout_add(PRIORITY_DEFAULT, delay=0).
|
|
98
|
-
* On Node.js: uses process.nextTick → queueMicrotask → Promise.resolve().then().
|
|
99
|
-
*/
|
|
100
|
-
export const nextTick = (fn: (...args: unknown[]) => void, ...args: unknown[]): void => {
|
|
101
|
-
const cb = args.length > 0 ? () => fn(...args) : fn as () => void;
|
|
102
|
-
if (tryGLibTimeout(cb)) return;
|
|
103
|
-
if (typeof globalThis.process?.nextTick === 'function') {
|
|
104
|
-
globalThis.process.nextTick(fn, ...args);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (typeof queueMicrotask === 'function') {
|
|
108
|
-
queueMicrotask(cb);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
Promise.resolve().then(cb);
|
|
112
|
-
};
|
package/src/path.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import Gio from '@girs/gio-2.0';
|
|
2
|
-
import GLib from '@girs/glib-2.0';
|
|
3
|
-
const { File } = Gio;
|
|
4
|
-
|
|
5
|
-
const _getProgramDir = (programFile: Gio.File) => {
|
|
6
|
-
const info = programFile.query_info('standard::', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
|
|
7
|
-
|
|
8
|
-
if (info.get_is_symlink()) {
|
|
9
|
-
const symlinkFile = programFile.get_parent().resolve_relative_path(info.get_symlink_target());
|
|
10
|
-
return symlinkFile.get_parent();
|
|
11
|
-
} else {
|
|
12
|
-
return programFile.get_parent();
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const resolve = (dir: string, ...filenames: string[]) => {
|
|
17
|
-
let file = File.new_for_path(dir);
|
|
18
|
-
for (const filename of filenames) {
|
|
19
|
-
file = file.resolve_relative_path(filename);
|
|
20
|
-
}
|
|
21
|
-
return file;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const getProgramExe = () => {
|
|
25
|
-
const currentDir = GLib.get_current_dir();
|
|
26
|
-
return File.new_for_path(currentDir).resolve_relative_path(imports.system.programInvocationName);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const getProgramDir = () => {
|
|
30
|
-
return _getProgramDir(getProgramExe()).get_path();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const getPathSeparator = () => {
|
|
34
|
-
const currentDir = GLib.get_current_dir();
|
|
35
|
-
return /^\//.test(currentDir) ? '/' : '\\';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const getNodeModulesPath = () => {
|
|
39
|
-
let dir = File.new_for_path(getProgramDir());
|
|
40
|
-
let found = false;
|
|
41
|
-
|
|
42
|
-
do {
|
|
43
|
-
dir = dir.resolve_relative_path("..");
|
|
44
|
-
const nodeModulesDir = dir.resolve_relative_path("node_modules");
|
|
45
|
-
found = nodeModulesDir.query_exists(null);
|
|
46
|
-
if (found) {
|
|
47
|
-
dir = nodeModulesDir;
|
|
48
|
-
}
|
|
49
|
-
} while (dir.has_parent(null) && !found)
|
|
50
|
-
|
|
51
|
-
return dir;
|
|
52
|
-
}
|