@gjsify/utils 0.3.21 → 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.
Files changed (46) hide show
  1. package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
  2. package/lib/esm/base64.js +1 -1
  3. package/lib/esm/byte-array.js +1 -1
  4. package/lib/esm/callable.js +1 -1
  5. package/lib/esm/cli.js +1 -1
  6. package/lib/esm/defer.js +1 -1
  7. package/lib/esm/encoding.js +1 -1
  8. package/lib/esm/error.js +1 -1
  9. package/lib/esm/file.js +1 -1
  10. package/lib/esm/fs.js +1 -1
  11. package/lib/esm/gio-errors.js +1 -1
  12. package/lib/esm/gio.js +1 -1
  13. package/lib/esm/globals.js +1 -1
  14. package/lib/esm/main-loop.js +1 -1
  15. package/lib/esm/message.js +1 -1
  16. package/lib/esm/microtask.js +1 -1
  17. package/lib/esm/next-tick.js +1 -1
  18. package/lib/esm/path.js +1 -1
  19. package/lib/esm/structured-clone.js +1 -1
  20. package/lib/types/callable.d.ts +25 -8
  21. package/lib/types/structured-clone.d.ts +12 -4
  22. package/package.json +44 -41
  23. package/src/base64.ts +0 -78
  24. package/src/byte-array.ts +0 -12
  25. package/src/callable.ts +0 -37
  26. package/src/cli.ts +0 -10
  27. package/src/defer.ts +0 -11
  28. package/src/encoding.ts +0 -36
  29. package/src/error.ts +0 -38
  30. package/src/file.ts +0 -12
  31. package/src/fs.ts +0 -24
  32. package/src/gio-errors.ts +0 -156
  33. package/src/gio.ts +0 -67
  34. package/src/globals.ts +0 -13
  35. package/src/index.ts +0 -18
  36. package/src/log.spec.ts +0 -32
  37. package/src/main-loop.ts +0 -62
  38. package/src/message.ts +0 -11
  39. package/src/microtask.ts +0 -4
  40. package/src/next-tick.spec.ts +0 -116
  41. package/src/next-tick.ts +0 -112
  42. package/src/path.ts +0 -52
  43. package/src/structured-clone.ts +0 -242
  44. package/src/test.ts +0 -5
  45. package/tsconfig.json +0 -38
  46. package/tsconfig.tsbuildinfo +0 -1
package/src/gio-errors.ts DELETED
@@ -1,156 +0,0 @@
1
- // Shared Gio.IOErrorEnum → Node.js error code mapping.
2
- // Used by fs, net, http, dns, child-process, and other packages that wrap Gio operations.
3
- //
4
- // The enum values are numeric constants from GLib — we use numbers directly
5
- // to avoid importing Gio just for error handling (keeps this usable in Node.js tests too).
6
-
7
- /** Map from Gio.IOErrorEnum numeric values to Node.js error code strings. */
8
- export const GIO_ERROR_TO_NODE: Record<number, string> = {
9
- 0: 'EIO', // FAILED
10
- 1: 'ENOENT', // NOT_FOUND
11
- 2: 'EEXIST', // EXISTS
12
- 3: 'EISDIR', // IS_DIRECTORY
13
- 4: 'ENOTDIR', // NOT_DIRECTORY
14
- 5: 'ENOTEMPTY', // NOT_EMPTY
15
- 6: 'ENOENT', // NOT_REGULAR_FILE
16
- 7: 'ENFILE', // TOO_MANY_OPEN_FILES
17
- 9: 'EACCES', // NOT_MOUNTABLE_FILE
18
- 10: 'ENFILE', // FILENAME_TOO_LONG
19
- 11: 'EINVAL', // INVALID_FILENAME
20
- 12: 'ELOOP', // TOO_MANY_LINKS
21
- 13: 'ENOSPC', // NO_SPACE
22
- 14: 'EACCES', // PERMISSION_DENIED
23
- 17: 'ELOOP', // TOO_MANY_LINKS (duplicate guard)
24
- 19: 'ENOSPC', // NO_SPACE (duplicate guard)
25
- 20: 'ENOTSUP', // NOT_SUPPORTED
26
- 22: 'EMFILE', // TOO_MANY_OPEN_FILES
27
- 24: 'EROFS', // READ_ONLY
28
- 25: 'ECANCELED', // CANCELLED
29
- 26: 'EBUSY', // BUSY
30
- 27: 'ETIMEDOUT', // TIMED_OUT
31
- 28: 'EHOSTUNREACH', // HOST_NOT_FOUND (was WOULD_BLOCK)
32
- 30: 'EHOSTUNREACH', // HOST_NOT_FOUND
33
- 31: 'ENETUNREACH', // NETWORK_UNREACHABLE
34
- 32: 'ECONNREFUSED', // CONNECTION_REFUSED (legacy value)
35
- 33: 'EADDRINUSE', // ADDRESS_IN_USE
36
- 34: 'ECONNRESET', // CONNECTION_CLOSED (mapped to reset)
37
- 36: 'EPIPE', // BROKEN_PIPE
38
- 38: 'ENETUNREACH', // NETWORK_UNREACHABLE (actual GJS value)
39
- 39: 'ECONNREFUSED', // CONNECTION_REFUSED (actual GJS value)
40
- 40: 'ECONNREFUSED', // PROXY_FAILED
41
- 41: 'EACCES', // PROXY_AUTH_FAILED
42
- 44: 'ECONNRESET', // CONNECTION_CLOSED (actual GJS value)
43
- 46: 'EMSGSIZE', // MESSAGE_TOO_LARGE
44
- };
45
-
46
- export interface NodeErrorDetails {
47
- path?: string;
48
- dest?: string;
49
- address?: string;
50
- port?: number;
51
- hostname?: string;
52
- }
53
-
54
- /** Node.js-style ErrnoException (defined locally to avoid @types/node dependency). */
55
- export interface ErrnoException extends Error {
56
- errno?: number;
57
- code?: string;
58
- path?: string;
59
- syscall?: string;
60
- address?: string;
61
- port?: number;
62
- hostname?: string;
63
- }
64
-
65
- /**
66
- * Create a Node.js-style ErrnoException from a Gio error.
67
- * Works for fs, net, dns, child-process, and other modules.
68
- */
69
- export function createNodeError(
70
- err: unknown,
71
- syscall: string,
72
- details?: NodeErrorDetails
73
- ): ErrnoException {
74
- const errObj = err as { code?: number; message?: string } | null | undefined;
75
- const code = GIO_ERROR_TO_NODE[errObj?.code ?? -1] || 'EIO';
76
-
77
- let msg = `${code}: ${errObj?.message || 'unknown error'}, ${syscall}`;
78
- if (details?.path) msg += ` '${details.path}'`;
79
- if (details?.dest) msg += ` -> '${details.dest}'`;
80
- if (details?.address) msg += ` ${details.address}`;
81
- if (details?.port != null) msg += `:${details.port}`;
82
-
83
- const error = new Error(msg) as ErrnoException;
84
- error.code = code;
85
- error.syscall = syscall;
86
- error.errno = -(errObj?.code || 0);
87
-
88
- if (details?.path) error.path = details.path;
89
- if (details?.address) error.address = details.address;
90
- if (details?.port != null) error.port = details.port;
91
-
92
- return error;
93
- }
94
-
95
- /**
96
- * Check if a Gio error is a "not found" error.
97
- */
98
- export function isNotFoundError(err: unknown): boolean {
99
- const errObj = err as { code?: number | string } | null | undefined;
100
- return errObj?.code === 1 || errObj?.code === 'ENOENT';
101
- }
102
-
103
- /**
104
- * Map from GLib.FileError numeric values to Node.js error code strings.
105
- * Distinct from Gio.IOErrorEnum — GLib.IOChannel.new_file() and some other
106
- * low-level GLib APIs throw GLib.FileError (domain "g-file-error"), which
107
- * has different numeric values than Gio.IOErrorEnum (domain "g-io-error-quark").
108
- */
109
- export const GLIB_FILE_ERROR_TO_NODE: Record<number, string> = {
110
- 0: 'EEXIST',
111
- 1: 'EISDIR',
112
- 2: 'EACCES',
113
- 3: 'ENAMETOOLONG',
114
- 4: 'ENOENT',
115
- 5: 'ENOTDIR',
116
- 6: 'ENXIO',
117
- 7: 'ENODEV',
118
- 8: 'EROFS',
119
- 11: 'ELOOP',
120
- 12: 'ENOSPC',
121
- 13: 'ENOMEM',
122
- 14: 'EMFILE',
123
- 15: 'ENFILE',
124
- 16: 'EBADF',
125
- 17: 'EINVAL',
126
- 18: 'EPIPE',
127
- 21: 'EIO',
128
- 22: 'EPERM',
129
- 24: 'EIO',
130
- };
131
-
132
- /**
133
- * Map a GLib.FileError to a Node.js-style ErrnoException. Counterpart to
134
- * `createNodeError` for the Gio.IOErrorEnum case; kept separate because the
135
- * enum domains differ.
136
- */
137
- export function createGLibFileError(
138
- err: unknown,
139
- syscall: string,
140
- details?: NodeErrorDetails,
141
- ): ErrnoException {
142
- const errObj = err as { code?: number; message?: string } | null | undefined;
143
- const code = GLIB_FILE_ERROR_TO_NODE[errObj?.code ?? -1] ?? 'EIO';
144
-
145
- let msg = `${code}: ${errObj?.message || 'unknown error'}, ${syscall}`;
146
- if (details?.path) msg += ` '${details.path}'`;
147
- if (details?.dest) msg += ` -> '${details.dest}'`;
148
-
149
- const error = new Error(msg) as ErrnoException;
150
- error.code = code;
151
- error.syscall = syscall;
152
- error.errno = -(errObj?.code || 0);
153
- if (details?.path) error.path = details.path;
154
-
155
- return error;
156
- }
package/src/gio.ts DELETED
@@ -1,67 +0,0 @@
1
- import Gio from '@girs/gio-2.0';
2
- import GLib from '@girs/glib-2.0';
3
-
4
- const byteArray = imports.byteArray;
5
-
6
- /**
7
- * Generic promise wrapper for Gio async/finish method pairs.
8
- *
9
- * Example:
10
- * const stream = await gioAsync<Gio.InputStream>(session, 'send_async', 'send_finish', msg, priority, null);
11
- */
12
- export function gioAsync<T>(
13
- obj: any,
14
- asyncMethod: string,
15
- finishMethod: string,
16
- ...args: any[]
17
- ): Promise<T> {
18
- return new Promise<T>((resolve, reject) => {
19
- obj[asyncMethod](...args, (_self: any, asyncRes: Gio.AsyncResult) => {
20
- try {
21
- resolve(obj[finishMethod](asyncRes));
22
- } catch (error) {
23
- reject(error);
24
- }
25
- });
26
- });
27
- }
28
-
29
- /**
30
- * Promise wrapper around `Gio.InputStream.read_bytes_async` / `read_bytes_finish`.
31
- * Returns a `Uint8Array` or `null` if the end of the stream is reached.
32
- */
33
- export async function readBytesAsync(
34
- inputStream: Gio.InputStream,
35
- count = 4096,
36
- ioPriority = GLib.PRIORITY_DEFAULT,
37
- cancellable: Gio.Cancellable | null = null
38
- ): Promise<Uint8Array | null> {
39
- return new Promise<Uint8Array | null>((resolve, reject) => {
40
- inputStream.read_bytes_async(count, ioPriority, cancellable, (_self, asyncRes) => {
41
- try {
42
- const res = inputStream.read_bytes_finish(asyncRes);
43
- if (res.get_size() === 0) {
44
- return resolve(null);
45
- }
46
- return resolve(byteArray.fromGBytes(res));
47
- } catch (error) {
48
- reject(error);
49
- }
50
- });
51
- });
52
- }
53
-
54
- /**
55
- * Async generator that yields `Uint8Array` chunks from a `Gio.InputStream`.
56
- */
57
- export async function* inputStreamAsyncIterator(
58
- inputStream: Gio.InputStream,
59
- count = 4096,
60
- ioPriority = GLib.PRIORITY_DEFAULT,
61
- cancellable: Gio.Cancellable | null = null
62
- ): AsyncGenerator<Uint8Array> {
63
- let chunk: Uint8Array | null;
64
- while ((chunk = await readBytesAsync(inputStream, count, ioPriority, cancellable)) !== null) {
65
- yield chunk;
66
- }
67
- }
package/src/globals.ts DELETED
@@ -1,13 +0,0 @@
1
- // Shared utility for registering global polyfills.
2
- // Used by web packages (abort-controller, dom-exception, streams, webcrypto, etc.)
3
- // to consistently register globals only when they're missing.
4
-
5
- /**
6
- * Register a value as a global property if it doesn't already exist.
7
- * This is a no-op in environments where the global is already defined (e.g. Node.js).
8
- */
9
- export function registerGlobal(name: string, value: unknown): void {
10
- if (typeof (globalThis as any)[name] === 'undefined') {
11
- (globalThis as any)[name] = value;
12
- }
13
- }
package/src/index.ts DELETED
@@ -1,18 +0,0 @@
1
- export * from './callable.js';
2
- export * from './base64.js';
3
- export * from './byte-array.js';
4
- export * from './cli.js';
5
- export * from './defer.js';
6
- export * from './encoding.js';
7
- export * from './globals.js';
8
- export * from './error.js';
9
- export * from './file.js';
10
- export * from './fs.js';
11
- export * from './gio.js';
12
- export * from './gio-errors.js';
13
- export * from './message.js';
14
- export * from './microtask.js';
15
- export * from './next-tick.js';
16
- export * from './path.js';
17
- export * from './structured-clone.js';
18
- export * from './main-loop.js';
package/src/log.spec.ts DELETED
@@ -1,32 +0,0 @@
1
- import { describe, it, assert, spy } from '@gjsify/unit';
2
- // import { logSignals } from '@gjsify/utils';
3
- // import type { StructuredLogData } from '@gjsify/utils';
4
-
5
- const createUncaughtException = async () => {
6
- throw new Error("top level error");
7
- }
8
-
9
- const sleep = (ms: number) => {
10
- return new Promise(resolve => setTimeout(resolve, ms))
11
- }
12
-
13
- export default async () => {
14
- await describe('logSignals', async () => {
15
- // TODO: Fix this test
16
-
17
- // await it("should emit an uncaughtException event on a top level throw", async () => {
18
- // const onUnhandledRejection = spy((_self, _data: StructuredLogData, _promiseData) => {});
19
-
20
- // const signalHandlerId = logSignals.connect("unhandledRejection", onUnhandledRejection);
21
-
22
- // createUncaughtException();
23
-
24
- // await sleep(10);
25
-
26
- // logSignals.disconnect(signalHandlerId)
27
-
28
- // assert.strictEqual(onUnhandledRejection.calls.length, 1, "onUnhandledRejection should be called.")
29
- // // assert.strictEqual(onUnhandledRejection.calls[0].arguments[0], error)
30
- // })
31
- });
32
- }
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
@@ -1,4 +0,0 @@
1
- // Unlike nextTick (GLib.idle_add, lets GTK events interleave), queueMicrotask fires before any GLib source.
2
- export const queueMicrotask = (fn: () => void): void => {
3
- Promise.resolve().then(fn);
4
- };
@@ -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
- }