@gjsify/effect-platform 0.48.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/README.md +103 -0
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/errors.js +1 -0
- package/lib/esm/file.js +1 -0
- package/lib/esm/filesystem.js +1 -0
- package/lib/esm/gio-async.js +1 -0
- package/lib/esm/gtk/scope.js +1 -0
- package/lib/esm/gtk/signal.js +1 -0
- package/lib/esm/gtk.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/path.js +1 -0
- package/lib/types/errors.d.ts +23 -0
- package/lib/types/errors.spec.d.ts +2 -0
- package/lib/types/file.d.ts +27 -0
- package/lib/types/filesystem.d.ts +14 -0
- package/lib/types/filesystem.spec.d.ts +2 -0
- package/lib/types/gio-async.d.ts +33 -0
- package/lib/types/gtk/scope.d.ts +43 -0
- package/lib/types/gtk/signal.d.ts +58 -0
- package/lib/types/gtk.d.ts +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/path.d.ts +4 -0
- package/lib/types/path.spec.d.ts +2 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @gjsify/effect-platform
|
|
2
|
+
|
|
3
|
+
[Effect](https://effect.website)'s platform services over GNOME. `effect/FileSystem` on
|
|
4
|
+
`Gio.File`, `effect/Path` on GLib, `GError` mapped onto Effect's normalized platform errors, and a
|
|
5
|
+
`/gtk` subpath that binds GObject lifetimes to Effect `Scope`s.
|
|
6
|
+
|
|
7
|
+
Effect 4 runs on GJS unmodified. This package is not what makes it run; it is what makes it
|
|
8
|
+
**GNOME**. See [ADR 0050](../../../docs/adr/0050-effect-platform-services-for-gnome.md) for why
|
|
9
|
+
that is a platform package rather than a fourth renderer.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Effect } from 'effect'
|
|
13
|
+
import * as FileSystem from 'effect/FileSystem'
|
|
14
|
+
import { fileSystemLayer } from '@gjsify/effect-platform'
|
|
15
|
+
|
|
16
|
+
const program = Effect.gen(function* () {
|
|
17
|
+
const fs = yield* FileSystem.FileSystem
|
|
18
|
+
return yield* fs.readDirectory('/etc')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
await Effect.runPromise(Effect.provide(program, fileSystemLayer))
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Two entries, split by what they import
|
|
25
|
+
|
|
26
|
+
| entry | provides | reaches |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `@gjsify/effect-platform` | `fileSystemLayer`, `pathLayer`, `reasonOf`, `isIoError`, `toPlatformError`, `gioAsync` | GLib, GIO |
|
|
29
|
+
| `@gjsify/effect-platform/gtk` | `widgetScope`, `windowScope`, `signalScope`, `runInScope`, `signalStream`, `propertyStream` | GTK, GObject |
|
|
30
|
+
|
|
31
|
+
The root entry declares `gjsify.headless` and CI walks its import graph to hold it. A service
|
|
32
|
+
layer that dragged in GTK could not be used from a daemon, a CLI or a test, which is where these
|
|
33
|
+
services earn their keep.
|
|
34
|
+
|
|
35
|
+
## What only this layer can do
|
|
36
|
+
|
|
37
|
+
**Cancellation reaches GIO.** `Effect.callback` hands its register function an `AbortSignal`;
|
|
38
|
+
every GIO async call takes a `Gio.Cancellable`. `gioAsync` wires the two, so interrupting a fiber
|
|
39
|
+
stops the in-flight read rather than discarding its result. Nothing promise-based can offer that.
|
|
40
|
+
|
|
41
|
+
**`effect/Path` is held against `node:path` operation by operation.** GLib has
|
|
42
|
+
`g_path_get_basename` and `g_path_get_dirname`, and they are not `path.posix`: measured,
|
|
43
|
+
`basename('')` is `.` where Node says `''`, and `dirname('a/b/')` is `a/b` where Node says `a`. The
|
|
44
|
+
differential in `tests/integration/effect` found eleven such classes that hand-written expectations
|
|
45
|
+
had all missed, which is why it exists.
|
|
46
|
+
|
|
47
|
+
**A `GError` becomes a tag you can branch on.** Today every failing GIO call is a `GLib.Error`
|
|
48
|
+
with a number whose meaning depends on its domain. Measured: `Gio.IOErrorEnum.NOT_FOUND` and
|
|
49
|
+
`GLib.FileError.ISDIR` are both `1`, so a code read without its domain reports *is a directory* as
|
|
50
|
+
*not found*. `reasonOf` refuses that, and a GIO code Effect has no tag for comes back `Unknown`
|
|
51
|
+
rather than as a near miss.
|
|
52
|
+
|
|
53
|
+
**A `Scope` closes with the widget.** `windowScope(window)` closes on `close-request` **or**
|
|
54
|
+
`destroy`, whichever is first, and everything forked into it is interrupted. Two triggers, because
|
|
55
|
+
`GtkWidget::destroy` is emitted from *dispose*: with the application holding a reference, which is
|
|
56
|
+
always, `gtk_window_destroy()` emits `unrealize` and no `destroy`. The full measured matrix is in
|
|
57
|
+
`src/gtk/scope.ts`.
|
|
58
|
+
|
|
59
|
+
## Conformance
|
|
60
|
+
|
|
61
|
+
Upstream authored `FileSystem.test-utils.ts` as a suite parameterised by a layer, so Node, Bun and
|
|
62
|
+
Deno answer the same questions. [`tests/integration/effect`](../../../tests/integration/effect)
|
|
63
|
+
ports it once and runs it twice: over `node:fs` and over this package's Gio layer. **Both pass all
|
|
64
|
+
of it**, including the cases about the file cursor that nobody here would have thought to ask —
|
|
65
|
+
seek backwards, `a+` with separate read and write positions, truncate under a live cursor. The
|
|
66
|
+
per-run counts live in
|
|
67
|
+
[`status/integration-coverage.md`](../../../status/integration-coverage.md), which is where a number
|
|
68
|
+
can be re-measured instead of remembered.
|
|
69
|
+
|
|
70
|
+
Three GIO semantics had to be corrected to get there, and each is recorded where it bites:
|
|
71
|
+
|
|
72
|
+
- `g_file_replace*` is GIO's *atomic replace*: it writes a temporary file and moves it into place
|
|
73
|
+
on close, so the path still holds the old bytes until then. POSIX `w`/`w+` truncate in place, so
|
|
74
|
+
those flags open read-write and truncate instead.
|
|
75
|
+
- A `GFileOutputStream` buffers where a descriptor does not, so every write flushes.
|
|
76
|
+
- `g_file_copy_async`/`move_async` are typed with `GObject.Closure` parameters by `@girs`, which
|
|
77
|
+
is not constructible from GJS in any reasonable way, so the layer takes the synchronous call.
|
|
78
|
+
**`copy`, `copyFile` and `rename` are therefore the three methods here that are NOT
|
|
79
|
+
interruptible** — the call blocks the fiber for its whole duration, so an interrupt cannot be
|
|
80
|
+
delivered until after it returns. Everything else goes through `gioAsync`.
|
|
81
|
+
|
|
82
|
+
## What it does not implement
|
|
83
|
+
|
|
84
|
+
`realPath`, `link` and `glob` raise a **defect**, not a failure — GIO has no symlink-resolving
|
|
85
|
+
canonicalizer, no hard-link call and no glob matcher. The distinction matters: `FileSystem.makeNoop`
|
|
86
|
+
would answer `remove()` with a silent success and the rest with `NotFound`, which is the tag a
|
|
87
|
+
genuinely missing file carries and which a `catchTag` would swallow. This package spells out the
|
|
88
|
+
complete interface through `FileSystem.make` so a method added upstream cannot arrive as a silent
|
|
89
|
+
default.
|
|
90
|
+
|
|
91
|
+
## Cost
|
|
92
|
+
|
|
93
|
+
Tree-shaking over Effect 4 works: you pay for what you import, and a GTK window using this layer
|
|
94
|
+
lands in the same size class as comparable showcases that use no Effect at all. The figures, and
|
|
95
|
+
which artifact each one came from, are in
|
|
96
|
+
[ADR 0050](../../../docs/adr/0050-effect-platform-services-for-gnome.md) § Cost.
|
|
97
|
+
|
|
98
|
+
## Peer dependency
|
|
99
|
+
|
|
100
|
+
`effect` is a **peer** dependency, pinned to the exact RC rather than a range. A consumer's Effect
|
|
101
|
+
and this layer's must be the same instance or the service keys do not match, and across prerelease
|
|
102
|
+
tags a range does not guarantee that: `>=4.0.0-rc.112` admits `4.0.0-rc.113`, which is free to move
|
|
103
|
+
a key. It relaxes to a caret the day 4.0.0 is stable.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import e from"gi://Gio?version=2.0";import t from"gi://GLib?version=2.0";import{PlatformError as n,SystemError as r}from"effect/PlatformError";const i=new Map([[e.IOErrorEnum.NOT_FOUND,`NotFound`],[e.IOErrorEnum.EXISTS,`AlreadyExists`],[e.IOErrorEnum.PERMISSION_DENIED,`PermissionDenied`],[e.IOErrorEnum.BUSY,`Busy`],[e.IOErrorEnum.TIMED_OUT,`TimedOut`],[e.IOErrorEnum.WOULD_BLOCK,`WouldBlock`],[e.IOErrorEnum.INVALID_DATA,`InvalidData`],[e.IOErrorEnum.CLOSED,`BadResource`],[e.IOErrorEnum.NOT_INITIALIZED,`BadResource`],[e.IOErrorEnum.NO_SPACE,`WriteZero`],[e.IOErrorEnum.PARTIAL_INPUT,`UnexpectedEof`]]),a=t.quark_to_string(e.io_error_quark()),isIoError=n=>n instanceof t.Error&&n.domain===e.io_error_quark(),reasonOf=e=>isIoError(e)?i.get(e.code)??`Unknown`:`Unknown`,toPlatformError=e=>new n(new r({_tag:reasonOf(e.error),module:e.module??`FileSystem`,method:e.method,pathOrDescriptor:e.pathOrDescriptor,description:describe(e.error),cause:e.error})),describe=e=>{if(e instanceof t.Error){let n=t.quark_to_string(e.domain)??String(e.domain);return n===a?e.message:`${n}(${e.code}): ${e.message}`}return e instanceof Error?e.message:String(e)};export{isIoError,reasonOf,toPlatformError};
|
package/lib/esm/file.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{toPlatformError as e}from"./errors.js";import{gioAsync as t}from"./gio-async.js";import n from"gi://Gio?version=2.0";import r from"gi://GLib?version=2.0";import{Effect as i,Option as a}from"effect";import*as o from"effect/FileSystem";const s={r:{readable:!0,append:!1},"r+":{readable:!0,append:!1},w:{readable:!1,append:!1},"w+":{readable:!0,append:!1},wx:{readable:!1,append:!1},"wx+":{readable:!0,append:!1},a:{readable:!1,append:!0},"a+":{readable:!0,append:!0},ax:{readable:!1,append:!0},"ax+":{readable:!0,append:!0}},openOrCreate=(a,o,s,c)=>i.gen(function*(){yield*i.ignore(t({method:`open`,path:o,source:a,start:(e,t,i)=>e.create_async(n.FileCreateFlags.NONE,r.PRIORITY_DEFAULT,t,(e,t)=>i(t)),finish:(e,t)=>e.create_finish(t)}));let l=yield*t({method:`open`,path:o,source:a,start:(e,t,n)=>e.open_readwrite_async(r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.open_readwrite_finish(t)});return c&&(yield*i.try({try:()=>l.truncate(0,null),catch:t=>e({method:`open`,pathOrDescriptor:o,error:t})})),{input:s.readable?l.get_input_stream():null,output:l.get_output_stream(),seekable:l,closable:l,plan:s}}),openStreams=(e,a)=>{let o=s[a],c=n.File.new_for_path(e),l=`open`,io=e=>({input:e.get_input_stream(),output:e.get_output_stream(),seekable:e,closable:e,plan:o});switch(a){case`r`:return i.map(t({method:l,path:e,source:c,start:(e,t,n)=>e.read_async(r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.read_finish(t)}),e=>({input:e,output:null,seekable:e,closable:e,plan:o}));case`r+`:return i.map(t({method:l,path:e,source:c,start:(e,t,n)=>e.open_readwrite_async(r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.open_readwrite_finish(t)}),io);case`w`:case`w+`:return openOrCreate(c,e,o,!0);case`wx`:case`ax`:return i.map(t({method:l,path:e,source:c,start:(e,t,i)=>e.create_async(n.FileCreateFlags.NONE,r.PRIORITY_DEFAULT,t,(e,t)=>i(t)),finish:(e,t)=>e.create_finish(t)}),e=>({input:null,output:e,seekable:e,closable:e,plan:o}));case`wx+`:case`ax+`:return i.map(t({method:l,path:e,source:c,start:(e,t,i)=>e.create_readwrite_async(n.FileCreateFlags.NONE,r.PRIORITY_DEFAULT,t,(e,t)=>i(t)),finish:(e,t)=>e.create_readwrite_finish(t)}),io);case`a`:return i.map(t({method:l,path:e,source:c,start:(e,t,i)=>e.append_to_async(n.FileCreateFlags.NONE,r.PRIORITY_DEFAULT,t,(e,t)=>i(t)),finish:(e,t)=>e.append_to_finish(t)}),e=>({input:null,output:e,seekable:e,closable:e,plan:o}));case`a+`:return openOrCreate(c,e,o,!1)}},makeFile=(s,c,l)=>{let u=0,fail=t=>n=>e({method:t,pathOrDescriptor:s,error:n,module:`FileSystem`}),seekTo=(e,t)=>i.try({try:()=>(c.seekable.seek(e,t,null),c.seekable.tell()),catch:fail(`seek`)}),readBytes=a=>{let o=c.input;return o===null?i.fail(e({method:`read`,pathOrDescriptor:s,module:`FileSystem`,error:r.Error.new_literal(n.io_error_quark(),n.IOErrorEnum.PERMISSION_DENIED,`file was not opened for reading`)})):i.gen(function*(){yield*seekTo(u,r.SeekType.SET);let e=(yield*t({method:`read`,path:s,source:o,start:(e,t,n)=>e.read_bytes_async(a,r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.read_bytes_finish(t)})).toArray();return u+=e.length,e})},writeBytes=a=>{let o=c.output;return o===null?i.fail(e({method:`write`,pathOrDescriptor:s,module:`FileSystem`,error:r.Error.new_literal(n.io_error_quark(),n.IOErrorEnum.PERMISSION_DENIED,`file was not opened for writing`)})):i.gen(function*(){yield*seekTo(c.plan.append?0:u,c.plan.append?r.SeekType.END:r.SeekType.SET);let e=yield*t({method:`write`,path:s,source:o,start:(e,t,n)=>e.write_bytes_async(new r.Bytes(a),r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.write_bytes_finish(t)});return yield*t({method:`write`,path:s,source:o,start:(e,t,n)=>e.flush_async(r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.flush_finish(t)}),c.plan.append||(u+=e),e})};return{[o.FileTypeId]:o.FileTypeId,stat:l,seek:(e,t)=>i.sync(()=>{let n=t===`start`?Number(e):u+Number(e);return u=n<0?0:n,o.Size(u)}),sync:i.gen(function*(){let e=c.output;e!==null&&(yield*t({method:`sync`,path:s,source:e,start:(e,t,n)=>e.flush_async(r.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.flush_finish(t)}))}),read:e=>i.map(readBytes(e.length),t=>(e.set(t),o.Size(t.length))),readAlloc:e=>i.map(readBytes(Number(e)),e=>e.length===0?a.none():a.some(e)),truncate:e=>i.gen(function*(){let t=e===void 0?0:Number(e);yield*i.try({try:()=>c.seekable.truncate(t,null),catch:fail(`truncate`)}),u>t&&(u=t)}),write:e=>i.map(writeBytes(e),o.Size),writeAll:e=>i.gen(function*(){let t=0;for(;t<e.length;)t+=yield*writeBytes(e.subarray(t))})}},openFile=(e,t,n)=>i.acquireRelease(i.map(openStreams(e,t?.flag??`r`),t=>({streams:t,file:makeFile(e,t,n(e))})),({streams:e})=>i.ignore(i.try(()=>e.closable.close(null)))).pipe(i.map(({file:e})=>e));export{makeFile,openFile};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{isIoError as e,toPlatformError as t}from"./errors.js";import{gioAsync as n}from"./gio-async.js";import{openFile as r}from"./file.js";import i from"gi://Gio?version=2.0";import a from"gi://GLib?version=2.0";import{badArgument as o}from"effect/PlatformError";import{Effect as s,Layer as c,Option as l,Result as u,Stream as d}from"effect";import*as f from"effect/FileSystem";import*as p from"effect/Queue";const m=[i.FILE_ATTRIBUTE_STANDARD_TYPE,i.FILE_ATTRIBUTE_STANDARD_SIZE,i.FILE_ATTRIBUTE_STANDARD_NAME,i.FILE_ATTRIBUTE_TIME_MODIFIED,i.FILE_ATTRIBUTE_TIME_ACCESS,i.FILE_ATTRIBUTE_TIME_CREATED,i.FILE_ATTRIBUTE_UNIX_MODE,i.FILE_ATTRIBUTE_UNIX_INODE,i.FILE_ATTRIBUTE_UNIX_NLINK,i.FILE_ATTRIBUTE_UNIX_UID,i.FILE_ATTRIBUTE_UNIX_GID,i.FILE_ATTRIBUTE_UNIX_DEVICE,i.FILE_ATTRIBUTE_UNIX_BLOCK_SIZE,i.FILE_ATTRIBUTE_UNIX_BLOCKS,i.FILE_ATTRIBUTE_ACCESS_CAN_READ,i.FILE_ATTRIBUTE_ACCESS_CAN_WRITE,i.FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE].join(`,`),fileFor=e=>i.File.new_for_path(e),queryInfo=(e,t,r=!0)=>n({method:t,path:e,source:fileFor(e),start:(e,t,n)=>e.query_info_async(m,r?i.FileQueryInfoFlags.NONE:i.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.query_info_finish(t)}),toDate=e=>e===null?l.none():l.some(new Date(e.to_unix()*1e3)),toNumber=e=>e===0?l.none():l.some(e),h=new Map([[i.FileType.REGULAR,`File`],[i.FileType.DIRECTORY,`Directory`],[i.FileType.SYMBOLIC_LINK,`SymbolicLink`],[i.FileType.SPECIAL,`Unknown`],[i.FileType.SHORTCUT,`Unknown`],[i.FileType.MOUNTABLE,`Unknown`],[i.FileType.UNKNOWN,`Unknown`]]),toInfo=e=>({type:h.get(e.get_file_type())??`Unknown`,mtime:toDate(e.get_modification_date_time()),atime:toDate(e.get_access_date_time()),birthtime:toDate(e.get_creation_date_time()),dev:e.get_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_DEVICE),ino:toNumber(Number(e.get_attribute_uint64(i.FILE_ATTRIBUTE_UNIX_INODE))),mode:e.get_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_MODE),nlink:toNumber(e.get_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_NLINK)),uid:toNumber(e.get_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_UID)),gid:toNumber(e.get_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_GID)),rdev:l.none(),size:f.Size(e.get_size()),blksize:l.some(f.Size(e.get_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_BLOCK_SIZE))),blocks:toNumber(Number(e.get_attribute_uint64(i.FILE_ATTRIBUTE_UNIX_BLOCKS)))}),readFile=e=>n({method:`readFile`,path:e,source:fileFor(e),start:(e,t,n)=>e.load_contents_async(t,(e,t)=>n(t)),finish:(e,t)=>{let[n,r]=e.load_contents_finish(t);if(!n)throw Error(`load_contents reported failure without raising`);return r}}),readDirectory=e=>s.scoped(readDirectoryScoped(e)),readDirectoryScoped=e=>s.gen(function*(){let t=yield*openEnumerator(e),r=[];for(;;){let i=yield*n({method:`readDirectory`,path:e,source:t,start:(e,t,n)=>e.next_files_async(64,a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.next_files_finish(t)});if(i.length===0)break;for(let e of i)r.push(e.get_name())}return r}),openEnumerator=e=>s.acquireRelease(n({method:`readDirectory`,path:e,source:fileFor(e),start:(e,t,n)=>e.enumerate_children_async(i.FILE_ATTRIBUTE_STANDARD_NAME,i.FileQueryInfoFlags.NONE,a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.enumerate_children_finish(t)}),e=>s.ignore(s.try(()=>e.close(null)))),dirnameOf=e=>a.path_get_dirname(e),makeTempDirectory=n=>s.try({try:()=>{let t=`${n?.prefix??`effect-platform-`}XXXXXX`;if(n?.directory===void 0)return a.Dir.make_tmp(t);for(let r=0;r<64;r++){let r=a.build_filenamev([n.directory,t.replace(`XXXXXX`,String(a.random_int()))]);try{return i.File.new_for_path(r).make_directory(null),r}catch(t){if(!e(t)||t.code!==i.IOErrorEnum.EXISTS)throw t}}throw Error(`no unused temporary directory name after 64 attempts`)},catch:e=>t({method:`makeTempDirectory`,error:e})}),makeTempFile=e=>s.flatMap(makeTempDirectory({directory:e?.directory,prefix:e?.prefix}),n=>s.try({try:()=>{let t=`${e?.prefix??`effect-platform-`}${a.random_int()}${e?.suffix??``}`,r=a.build_filenamev([n,t]);return i.File.new_for_path(r).create(i.FileCreateFlags.NONE,null).close(null),r},catch:e=>t({method:`makeTempFile`,error:e})})),remove=(e,t)=>s.gen(function*(){if(t?.recursive===!0){let n=yield*s.result(queryInfo(e,`remove`,!1));if(u.isSuccess(n)&&n.success.get_file_type()===i.FileType.DIRECTORY)for(let n of yield*readDirectory(e))yield*remove(a.build_filenamev([e,n]),t)}let r=yield*s.result(s.asVoid(n({method:`remove`,path:e,source:fileFor(e),start:(e,t,n)=>e.delete_async(a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.delete_finish(t)})));if(u.isFailure(r)&&!(r.failure.reason._tag===`NotFound`&&(t?.force===!0||t?.recursive===!0)))return yield*s.fail(r.failure)}),copy=(e,n,r,c)=>s.gen(function*(){if(n===e||n.startsWith(`${e}${a.DIR_SEPARATOR_S}`))return yield*s.fail(o({module:`FileSystem`,method:c,description:`cannot copy a path into itself`}));if((yield*queryInfo(e,c,!1)).get_file_type()===i.FileType.DIRECTORY){yield*s.try({try:()=>{fileFor(n).make_directory_with_parents(null)},catch:e=>t({method:c,pathOrDescriptor:n,error:e})}).pipe(s.catchTag(`PlatformError`,e=>e.reason._tag===`AlreadyExists`?s.void:s.fail(e)));for(let t of yield*readDirectory(e))yield*copy(a.build_filenamev([e,t]),a.build_filenamev([n,t]),r,c);return}yield*copyOne(e,n,r,c)}),copyOne=(e,n,r,a)=>s.try({try:()=>{let t=i.FileCopyFlags.NONE;r?.overwrite===!0&&(t|=i.FileCopyFlags.OVERWRITE),r?.preserveTimestamps!==!0&&(t|=i.FileCopyFlags.TARGET_DEFAULT_MODIFIED_TIME),t|=i.FileCopyFlags.NOFOLLOW_SYMLINKS,fileFor(e).copy(fileFor(n),t,null,null)},catch:n=>t({method:a,pathOrDescriptor:e,error:n})}),watch=e=>d.callback(n=>s.gen(function*(){let r=yield*s.try({try:()=>fileFor(e).monitor(i.FileMonitorFlags.WATCH_MOVES,null),catch:n=>t({method:`watch`,pathOrDescriptor:e,error:n})}),a=r.connect(`changed`,(t,r,i,a)=>{let o=r.get_path()??e,s=g.get(a);s!==void 0&&p.offerUnsafe(n,s(o))});yield*s.addFinalizer(()=>s.sync(()=>{r.disconnect(a),r.cancel()}))})),g=new Map([[i.FileMonitorEvent.CREATED,e=>({_tag:`Create`,path:e})],[i.FileMonitorEvent.CHANGED,e=>({_tag:`Update`,path:e})],[i.FileMonitorEvent.ATTRIBUTE_CHANGED,e=>({_tag:`Update`,path:e})],[i.FileMonitorEvent.DELETED,e=>({_tag:`Remove`,path:e})],[i.FileMonitorEvent.MOVED_OUT,e=>({_tag:`Remove`,path:e})],[i.FileMonitorEvent.MOVED_IN,e=>({_tag:`Create`,path:e})]]),setAttribute=(e,t,r,o)=>s.gen(function*(){let s=yield*n({method:t,path:e,source:fileFor(e),start:(e,t,n)=>e.query_info_async(r,i.FileQueryInfoFlags.NONE,a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.query_info_finish(t)});o(s),yield*n({method:t,path:e,source:fileFor(e),start:(e,t,n)=>e.set_attributes_async(s,i.FileQueryInfoFlags.NONE,a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.set_attributes_finish(t)[1]})}),unimplemented=e=>s.die(Error(`@gjsify/effect-platform: FileSystem.${e} has no GIO equivalent`)),makeGioFileSystem=()=>f.make({access:(e,n)=>s.gen(function*(){let r=yield*queryInfo(e,`access`),denied=n=>s.fail(t({method:`access`,pathOrDescriptor:e,error:a.Error.new_literal(i.io_error_quark(),i.IOErrorEnum.PERMISSION_DENIED,`not ${n}`)}));if(n?.readable===!0&&!r.get_attribute_boolean(i.FILE_ATTRIBUTE_ACCESS_CAN_READ))return yield*denied(`readable`);if(n?.writable===!0&&!r.get_attribute_boolean(i.FILE_ATTRIBUTE_ACCESS_CAN_WRITE))return yield*denied(`writable`)}),stat:e=>s.map(queryInfo(e,`stat`),toInfo),readFile,readDirectory,readLink:e=>s.flatMap(queryInfo(e,`readLink`,!1),n=>{let r=n.get_symlink_target();return r===null?s.fail(t({method:`readLink`,pathOrDescriptor:e,error:a.Error.new_literal(i.io_error_quark(),i.IOErrorEnum.INVALID_ARGUMENT,`not a symbolic link`)})):s.succeed(r)}),realPath:()=>unimplemented(`realPath`),link:()=>unimplemented(`link`),glob:()=>unimplemented(`glob`),open:(e,t)=>r(e,t,e=>s.map(queryInfo(e,`stat`),toInfo)),makeDirectory:(e,r)=>r?.recursive===!0?s.try({try:()=>{fileFor(e).make_directory_with_parents(null)},catch:n=>t({method:`makeDirectory`,pathOrDescriptor:e,error:n})}):s.asVoid(n({method:`makeDirectory`,path:e,source:fileFor(e),start:(e,t,n)=>e.make_directory_async(a.PRIORITY_DEFAULT,t,(e,t)=>n(t)),finish:(e,t)=>e.make_directory_finish(t)})),remove:(e,t)=>remove(e,t),rename:(e,n)=>s.try({try:()=>{fileFor(e).move(fileFor(n),i.FileCopyFlags.OVERWRITE,null,null)},catch:n=>t({method:`rename`,pathOrDescriptor:e,error:n})}),copy:(e,t,n)=>copy(e,t,n,`copy`),copyFile:(e,t)=>copy(e,t,void 0,`copyFile`),symlink:(e,t)=>s.asVoid(n({method:`symlink`,path:t,source:fileFor(t),start:(t,n,r)=>t.make_symbolic_link_async(e,a.PRIORITY_DEFAULT,n,(e,t)=>r(t)),finish:(e,t)=>e.make_symbolic_link_finish(t)})),truncate:(e,t)=>s.scoped(s.flatMap(r(e,{flag:`r+`},e=>s.map(queryInfo(e,`stat`),toInfo)),e=>e.truncate(t))),chmod:(e,t)=>setAttribute(e,`chmod`,i.FILE_ATTRIBUTE_UNIX_MODE,e=>{e.set_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_MODE,t)}),chown:(e,t,n)=>setAttribute(e,`chown`,i.FILE_ATTRIBUTE_UNIX_UID,e=>{e.set_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_UID,t),e.set_attribute_uint32(i.FILE_ATTRIBUTE_UNIX_GID,n)}),utimes:(e,t,n)=>setAttribute(e,`utimes`,i.FILE_ATTRIBUTE_TIME_MODIFIED,e=>{let seconds=e=>BigInt(Math.floor(Number(e instanceof Date?e.getTime():e)/1e3));e.set_attribute_uint64(i.FILE_ATTRIBUTE_TIME_ACCESS,seconds(t)),e.set_attribute_uint64(i.FILE_ATTRIBUTE_TIME_MODIFIED,seconds(n))}),makeTempDirectory:e=>makeTempDirectory(e),makeTempDirectoryScoped:e=>s.acquireRelease(makeTempDirectory(e),e=>s.ignore(remove(e,{recursive:!0}))),makeTempFile:e=>makeTempFile(e),makeTempFileScoped:e=>s.acquireRelease(makeTempFile(e),e=>s.ignore(remove(dirnameOf(e),{recursive:!0}))),writeFile:(e,n,i)=>s.scoped(s.flatMap(r(e,{flag:i?.flag??`w`},e=>s.map(queryInfo(e,`stat`),toInfo)),e=>e.writeAll(n))).pipe(s.mapError(n=>n.reason._tag===`BadArgument`?n:t({method:`writeFile`,pathOrDescriptor:e,error:n.reason.cause??n}))),watch:e=>watch(e)}),_=c.sync(f.FileSystem)(makeGioFileSystem);export{_ as layer,makeGioFileSystem};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{toPlatformError as e}from"./errors.js";import t from"gi://Gio?version=2.0";import{Effect as n}from"effect";const gioAsync=r=>n.callback((i,a)=>{let o=new t.Cancellable,onAbort=()=>o.cancel();a.addEventListener(`abort`,onAbort),r.start(r.source,o,t=>{if(a.removeEventListener(`abort`,onAbort),!o.is_cancelled())try{i(n.succeed(r.finish(r.source,t)))}catch(t){i(n.fail(e({method:r.method,pathOrDescriptor:r.path,error:t})))}})});export{gioAsync};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"../_virtual/_rolldown/runtime.js";import{Effect as e,Exit as t,Scope as n}from"effect";const signalScope=(r,i)=>{let a=n.makeUnsafe(`sequential`),o=!1,s=[],disconnect=()=>{for(let e of s)r.disconnect(e);s=[]},close=()=>{if(o)return;o=!0,disconnect();let r=n.closeUnsafe(a,t.void);r!==void 0&&e.runFork(r)};return s=i.map(e=>r.connect(e,()=>(close(),!1))),{scope:a,close}},widgetScope=e=>signalScope(e,[`destroy`]),windowScope=e=>signalScope(e,[`close-request`,`destroy`]),runInScope=(t,n)=>e.runSync(e.forkIn(n,t));export{runInScope,signalScope,widgetScope,windowScope};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"../_virtual/_rolldown/runtime.js";import{Effect as e,Stream as t}from"effect";import*as n from"effect/Queue";const signalStream=(r,i,a={})=>t.callback(t=>e.gen(function*(){let a=r.connect(i,(e,...r)=>{n.offerUnsafe(t,r)});yield*e.addFinalizer(()=>e.sync(()=>r.disconnect(a)))}),{bufferSize:a.bufferSize??16,strategy:a.strategy??`sliding`}),propertyStream=(e,n,r,i={})=>t.concat(t.sync(r),t.map(signalStream(e,`notify::${n}`,i),r));export{propertyStream,signalStream};
|
package/lib/esm/gtk.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{runInScope as e,signalScope as t,widgetScope as n,windowScope as r}from"./gtk/scope.js";import{propertyStream as i,signalStream as a}from"./gtk/signal.js";export{i as propertyStream,e as runInScope,t as signalScope,a as signalStream,n as widgetScope,r as windowScope};
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isIoError as e,reasonOf as t,toPlatformError as n}from"./errors.js";import{gioAsync as r}from"./gio-async.js";import{layer as i,makeGioFileSystem as a}from"./filesystem.js";import{layer as o}from"./path.js";export{i as fileSystemLayer,r as gioAsync,e as isIoError,a as makeGioFileSystem,o as pathLayer,t as reasonOf,n as toPlatformError};
|
package/lib/esm/path.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import e from"gi://GLib?version=2.0";import{BadArgument as t}from"effect/PlatformError";import{Effect as n,Layer as r}from"effect";import{Path as i,TypeId as a}from"effect/Path";const o=e.DIR_SEPARATOR_S,isAbsolute=t=>e.path_is_absolute(t),join=(...t)=>{let n=t.filter(e=>e.length>0);return n.length===0?`.`:normalize(e.build_filenamev(n))},resolve=(...t)=>{let n=e.get_current_dir();for(let r of t)r.length!==0&&(n=isAbsolute(r)?r:e.build_filenamev([n,r]));return e.canonicalize_filename(n,null).replace(/^\/\/+/,o)},normalize=e=>{if(e.length===0)return`.`;let t=isAbsolute(e),n=e.length>1&&e.endsWith(o),r=[];for(let n of e.split(o))if(!(n===``||n===`.`)){if(n!==`..`){r.push(n);continue}r.length>0&&r[r.length-1]!==`..`?r.pop():t||r.push(`..`)}let i=r.join(o);return t?n&&i.length>0?`${o}${i}${o}`:`${o}${i}`:i.length===0?`.`:n?`${i}${o}`:i},basename=(e,t)=>{let n=e.replace(/\/+$/,``),r=n.slice(n.lastIndexOf(o)+1);return t!==void 0&&t!==r&&r.endsWith(t)?r.slice(0,r.length-t.length):r},dirname=e=>{if(e.length===0)return`.`;let t=isAbsolute(e),n=e.replace(/\/+$/,``);if(n.length===0)return o;let r=n.lastIndexOf(o);return r<0?`.`:r===0?o:t||r>0?n.slice(0,r):`.`},extname=e=>{let t=basename(e);if(t===`.`||t===`..`)return``;let n=t.lastIndexOf(`.`);return n<=0?``:t.slice(n)},parse=e=>{let t=isAbsolute(e)?o:``,n=basename(e),r=extname(e),i=e.replace(/\/+$/,``).lastIndexOf(o);return{root:t,dir:n.length===0?t===o?o:``:i<=0?t:e.slice(0,i),base:n,ext:r,name:r.length===0?n:n.slice(0,n.length-r.length)}},format=e=>{let t=e.base??`${e.name??``}${e.ext??``}`,n=e.dir??e.root??``;return n.length===0?t:n===e.root?`${n}${t}`:`${n}${o}${t}`},relative=(e,t)=>{let n=resolve(e).split(o).filter(Boolean),r=resolve(t).split(o).filter(Boolean),i=0;for(;i<n.length&&i<r.length&&n[i]===r[i];)i++;return[...Array(n.length-i).fill(`..`),...r.slice(i)].join(o)},fromFileUrl=r=>n.try({try:()=>e.filename_from_uri(r.href)[0],catch:e=>new t({module:`Path`,method:`fromFileUrl`,cause:e})}),toFileUrl=r=>n.try({try:()=>new URL(e.filename_to_uri(resolve(r),null)),catch:e=>new t({module:`Path`,method:`toFileUrl`,cause:e})}),s=r.succeed(i)({[a]:a,sep:o,basename,dirname,extname,format,fromFileUrl,isAbsolute,join,normalize,parse,relative,resolve,toFileUrl,toNamespacedPath:e=>e});export{s as layer};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import GLib from 'gi://GLib?version=2.0';
|
|
2
|
+
import { PlatformError, type SystemErrorTag } from 'effect/PlatformError';
|
|
3
|
+
/** `true` when this value is a `GLib.Error` raised by GIO itself. */
|
|
4
|
+
export declare const isIoError: (error: unknown) => error is GLib.Error;
|
|
5
|
+
/**
|
|
6
|
+
* The normalized reason for a caught value. Non-GIO errors — a `TypeError` from
|
|
7
|
+
* our own binding code, a `GLib.Error` from another domain — are `Unknown` rather
|
|
8
|
+
* than guessed at.
|
|
9
|
+
*/
|
|
10
|
+
export declare const reasonOf: (error: unknown) => SystemErrorTag;
|
|
11
|
+
/**
|
|
12
|
+
* Wrap a caught GIO failure as the platform error `effect/FileSystem` declares.
|
|
13
|
+
*
|
|
14
|
+
* `module` is the Effect service that failed, not the GIO class, so a consumer
|
|
15
|
+
* reading `NotFound: FileSystem.readFile (/etc/nope)` cannot tell which layer
|
|
16
|
+
* produced it — which is the point.
|
|
17
|
+
*/
|
|
18
|
+
export declare const toPlatformError: (options: {
|
|
19
|
+
readonly method: string;
|
|
20
|
+
readonly pathOrDescriptor?: string | number;
|
|
21
|
+
readonly error: unknown;
|
|
22
|
+
readonly module?: string;
|
|
23
|
+
}) => PlatformError;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Gio from 'gi://Gio?version=2.0';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import type * as Scope from 'effect/Scope';
|
|
4
|
+
import * as FileSystem from 'effect/FileSystem';
|
|
5
|
+
import type { PlatformError } from 'effect/PlatformError';
|
|
6
|
+
/** How a flag maps onto GIO: which streams it opens, and whether writes append. */
|
|
7
|
+
export interface OpenPlan {
|
|
8
|
+
readonly readable: boolean;
|
|
9
|
+
/** Writes go to the end of the file regardless of the read position. */
|
|
10
|
+
readonly append: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** The GIO streams behind one open file, plus what the flag asked for. */
|
|
13
|
+
interface Streams {
|
|
14
|
+
readonly input: Gio.InputStream | null;
|
|
15
|
+
readonly output: Gio.OutputStream | null;
|
|
16
|
+
readonly seekable: Gio.Seekable;
|
|
17
|
+
readonly closable: Gio.IOStream | Gio.InputStream | Gio.OutputStream;
|
|
18
|
+
readonly plan: OpenPlan;
|
|
19
|
+
}
|
|
20
|
+
/** Build the `File` handle Effect's `open` promises. */
|
|
21
|
+
export declare const makeFile: (path: string, streams: Streams, stat: Effect.Effect<FileSystem.File.Info, PlatformError>) => FileSystem.File;
|
|
22
|
+
/** Open `path` and close its streams when the scope closes. */
|
|
23
|
+
export declare const openFile: (path: string, options: {
|
|
24
|
+
readonly flag?: FileSystem.OpenFlag | undefined;
|
|
25
|
+
readonly mode?: number | undefined;
|
|
26
|
+
} | undefined, stat: (path: string) => Effect.Effect<FileSystem.File.Info, PlatformError>) => Effect.Effect<FileSystem.File, PlatformError, Scope.Scope>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Layer } from 'effect';
|
|
2
|
+
import * as FileSystem from 'effect/FileSystem';
|
|
3
|
+
/**
|
|
4
|
+
* The Gio implementation.
|
|
5
|
+
*
|
|
6
|
+
* `FileSystem.make` and not `makeNoop`: `make` demands the COMPLETE interface, so a
|
|
7
|
+
* method added upstream cannot arrive here as a silent default. It derives `exists`
|
|
8
|
+
* from `access`, `readFileString` from `readFile`, and `stream`/`sink` from `open`,
|
|
9
|
+
* all four of which are implemented — so those derivations are correct here for
|
|
10
|
+
* free rather than inheriting a stub.
|
|
11
|
+
*/
|
|
12
|
+
export declare const makeGioFileSystem: () => FileSystem.FileSystem;
|
|
13
|
+
/** Provide the Gio-backed `FileSystem` to a program. */
|
|
14
|
+
export declare const layer: Layer.Layer<FileSystem.FileSystem>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Gio from 'gi://Gio?version=2.0';
|
|
2
|
+
import type GObject from 'gi://GObject?version=2.0';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import type { PlatformError } from 'effect/PlatformError';
|
|
5
|
+
/**
|
|
6
|
+
* The one adapter every method below goes through: a GIO async pair
|
|
7
|
+
* (`x_async`/`x_finish`) as an interruptible Effect.
|
|
8
|
+
*
|
|
9
|
+
* `Effect.callback`'s `signal` is what makes it interruptible for real. Aborting it
|
|
10
|
+
* cancels the `Gio.Cancellable`, GIO completes the operation with
|
|
11
|
+
* `Gio.IOErrorEnum.CANCELLED`, and the `finish` call throws it — at which point
|
|
12
|
+
* nobody is listening, because `resume` after an interrupt is a no-op. So the
|
|
13
|
+
* catch is not swallowing an error, it is declining to report one for work whose
|
|
14
|
+
* requester is gone; anything else would be a defect raised in a dead fiber.
|
|
15
|
+
*
|
|
16
|
+
* `source` IS A PARAMETER, and it has to be. `g_task_is_valid(result, source)`
|
|
17
|
+
* checks that the object finishing an operation is the object that started it, and
|
|
18
|
+
* `Gio.File.new_for_path(p)` returns a NEW GFile every call — two files for the
|
|
19
|
+
* same path are not the same source. Measured on the first run of this showcase:
|
|
20
|
+
* a `finish` on a freshly constructed GFile logged
|
|
21
|
+
* `g_file_real_enumerate_children_finish: assertion 'g_task_is_valid (res, file)'
|
|
22
|
+
* failed`, returned `null`, and the failure surfaced one call later as
|
|
23
|
+
* `can't access property "next_files_async", r is null` — a null-dereference that
|
|
24
|
+
* names nothing about the actual mistake.
|
|
25
|
+
*/
|
|
26
|
+
export declare const gioAsync: <A, S extends GObject.Object>(options: {
|
|
27
|
+
readonly method: string;
|
|
28
|
+
readonly path: string;
|
|
29
|
+
/** The object the operation is started ON, and finished on. */
|
|
30
|
+
readonly source: S;
|
|
31
|
+
readonly start: (source: S, cancellable: Gio.Cancellable, done: (result: Gio.AsyncResult) => void) => void;
|
|
32
|
+
readonly finish: (source: S, result: Gio.AsyncResult) => A;
|
|
33
|
+
}) => Effect.Effect<A, PlatformError>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type GObject from 'gi://GObject?version=2.0';
|
|
2
|
+
import type Gtk from 'gi://Gtk?version=4.0';
|
|
3
|
+
import { Effect, Scope } from 'effect';
|
|
4
|
+
import type * as Fiber from 'effect/Fiber';
|
|
5
|
+
/** A scope plus the means to close it early and stop listening. */
|
|
6
|
+
export interface WidgetScope {
|
|
7
|
+
readonly scope: Scope.Closeable;
|
|
8
|
+
/** Close now and disconnect the triggers. Idempotent. */
|
|
9
|
+
readonly close: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A `Scope` closed by one or more signals on `source`, whichever fires first.
|
|
13
|
+
*
|
|
14
|
+
* Shared by the two functions below, and exported because a widget with a
|
|
15
|
+
* different end-of-life signal — a `Gtk.ListItem` being recycled, a page leaving
|
|
16
|
+
* an `Adw.NavigationView` — needs the same thing with a different name.
|
|
17
|
+
*/
|
|
18
|
+
export declare const signalScope: (source: GObject.Object, signals: ReadonlyArray<string>) => WidgetScope;
|
|
19
|
+
/**
|
|
20
|
+
* A `Scope` closed when `widget` is DISPOSED — the boundary after which no fiber
|
|
21
|
+
* may touch it.
|
|
22
|
+
*
|
|
23
|
+
* Note what this does not do: `gtk_window_destroy()` does not emit `destroy` while
|
|
24
|
+
* a reference is held, so a window closed by the application keeps this scope open.
|
|
25
|
+
* For a window, use {@link windowScope}.
|
|
26
|
+
*/
|
|
27
|
+
export declare const widgetScope: (widget: Gtk.Widget) => WidgetScope;
|
|
28
|
+
/**
|
|
29
|
+
* A `Scope` closed when `window` leaves the screen OR is disposed, whichever comes
|
|
30
|
+
* first — the lifetime an application means when it says "while this window is
|
|
31
|
+
* open".
|
|
32
|
+
*/
|
|
33
|
+
export declare const windowScope: (window: Gtk.Window) => WidgetScope;
|
|
34
|
+
/**
|
|
35
|
+
* Start `effect` as a fiber owned by `scope`. Closing the scope interrupts it, and
|
|
36
|
+
* its finalizers run before the close completes.
|
|
37
|
+
*
|
|
38
|
+
* Returns the `Fiber` rather than a promise on purpose: the caller is a signal
|
|
39
|
+
* handler that must return NOW, and a promise there is a value GTK will
|
|
40
|
+
* misinterpret. If you need the outcome, that is `fiber.addObserver(exit => …)`,
|
|
41
|
+
* which is synchronous and one line.
|
|
42
|
+
*/
|
|
43
|
+
export declare const runInScope: <A, E>(scope: Scope.Scope, effect: Effect.Effect<A, E, never>) => Fiber.Fiber<A, E>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type GObject from 'gi://GObject?version=2.0';
|
|
2
|
+
import { Stream } from 'effect';
|
|
3
|
+
export interface SignalStreamOptions {
|
|
4
|
+
/** Queue depth. Small on purpose: a UI signal's backlog has no value. */
|
|
5
|
+
readonly bufferSize?: number;
|
|
6
|
+
/**
|
|
7
|
+
* What a full buffer does. `'sliding'` drops the oldest; `'suspend'` blocks the
|
|
8
|
+
* emitter, which for a main-loop signal means the window stops drawing.
|
|
9
|
+
*
|
|
10
|
+
* `'suspend'` is offered rather than forbidden because the type is `Queue`'s own
|
|
11
|
+
* and a non-main-loop `GObject` — a worker's `Gio.Task`, an `EventBus` — can
|
|
12
|
+
* legitimately want it. For anything GTK emits it is the wrong answer.
|
|
13
|
+
*/
|
|
14
|
+
readonly strategy?: 'sliding' | 'suspend';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Every emission of `signalName` on `source`, as a `Stream` of the handler's
|
|
18
|
+
* arguments (the emitting object itself is dropped, as it is always `source`).
|
|
19
|
+
*
|
|
20
|
+
* The handler returns nothing, so this is only correct for signals whose return
|
|
21
|
+
* value GTK ignores. A signal with a meaningful return — `key-pressed`,
|
|
22
|
+
* `close-request` — must be handled synchronously; see `runInScope` in `scope.ts`
|
|
23
|
+
* for the split.
|
|
24
|
+
*
|
|
25
|
+
* The connection is a resource: `Effect.addFinalizer` disconnects it when the
|
|
26
|
+
* stream's scope closes, so a stream held by a widget scope stops listening the
|
|
27
|
+
* moment the widget is destroyed.
|
|
28
|
+
*
|
|
29
|
+
* SUBSCRIBING IS ASYNCHRONOUS, and nothing buffers an emission that beats it. The
|
|
30
|
+
* `connect()` below runs when the stream is first PULLED, not when it is
|
|
31
|
+
* constructed and not when a fiber running it is forked — so an emission between
|
|
32
|
+
* those two moments is simply not seen. In an application this is invisible,
|
|
33
|
+
* because the subscription is set up in a constructor and the first emission comes
|
|
34
|
+
* from a user; it is visible the moment a test emits programmatically, and the
|
|
35
|
+
* showcase's probe has to sleep before it emits for that reason.
|
|
36
|
+
*/
|
|
37
|
+
export declare const signalStream: <A extends ReadonlyArray<unknown> = ReadonlyArray<unknown>>(source: GObject.Object, signalName: string, options?: SignalStreamOptions) => Stream.Stream<A>;
|
|
38
|
+
/**
|
|
39
|
+
* The common special case: a GObject property, as a stream of its values, starting
|
|
40
|
+
* with the value it has now.
|
|
41
|
+
*
|
|
42
|
+
* Starting with the current value is what makes this usable as a source of truth
|
|
43
|
+
* rather than a change log — a consumer subscribing after the property was already
|
|
44
|
+
* set otherwise renders an empty view until the user types again. It does NOT close
|
|
45
|
+
* the subscription gap above: `Stream.concat` reads the seed before the second
|
|
46
|
+
* stream is pulled, so a change landing in between is still missed. What it fixes is
|
|
47
|
+
* the steady state, not the race.
|
|
48
|
+
*
|
|
49
|
+
* `read` IS EXPLICIT, and both reasons are worth stating. `GObject.Object.get_property`
|
|
50
|
+
* is the GIR method, which takes a name AND a `GObject.Value` to fill — a
|
|
51
|
+
* one-argument call throws `At least 2 arguments required, but only 1 passed`,
|
|
52
|
+
* measured here on the showcase's first probe run. And even given a working
|
|
53
|
+
* reflective read, the GObject name and the JS name differ (`icon-name` is
|
|
54
|
+
* `iconName`), so a helper that mapped between them would be guessing at a
|
|
55
|
+
* convention that has exceptions. Passing the getter costs one closure and is
|
|
56
|
+
* typed.
|
|
57
|
+
*/
|
|
58
|
+
export declare const propertyStream: <T>(source: GObject.Object, property: string, read: () => T, options?: SignalStreamOptions) => Stream.Stream<T>;
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/effect-platform",
|
|
3
|
+
"version": "0.48.0",
|
|
4
|
+
"description": "Effect's platform services over GNOME: effect/FileSystem on Gio.File with cancellation that reaches GIO, effect/Path on GLib, GError mapped onto Effect's normalized platform errors, and a /gtk subpath binding GObject lifetimes to Effect Scopes",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect",
|
|
7
|
+
"gjs",
|
|
8
|
+
"gnome",
|
|
9
|
+
"gio",
|
|
10
|
+
"glib",
|
|
11
|
+
"gtk",
|
|
12
|
+
"filesystem",
|
|
13
|
+
"platform"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "lib/esm/index.js",
|
|
17
|
+
"module": "lib/esm/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/esm/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./gtk": {
|
|
25
|
+
"types": "./lib/types/gtk.d.ts",
|
|
26
|
+
"default": "./lib/esm/gtk.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"files": [
|
|
32
|
+
"lib"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clear": "gjsify clear lib tmp tsconfig.tsbuildinfo test.gjs.mjs",
|
|
36
|
+
"check": "gjsify tsc --noEmit",
|
|
37
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
38
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
39
|
+
"build:types": "gjsify tsc",
|
|
40
|
+
"build:test": "gjsify run build:test:gjs",
|
|
41
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
42
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
|
|
43
|
+
"test:gjs": "gjsify run test.gjs.mjs"
|
|
44
|
+
},
|
|
45
|
+
"gjsify": {
|
|
46
|
+
"runtimes": {
|
|
47
|
+
"gjs": "polyfill",
|
|
48
|
+
"node": "none",
|
|
49
|
+
"browser": "none",
|
|
50
|
+
"nativescript": "none"
|
|
51
|
+
},
|
|
52
|
+
"tier": 3,
|
|
53
|
+
"headless": [
|
|
54
|
+
"Gdk",
|
|
55
|
+
"GdkPixbuf",
|
|
56
|
+
"Gsk",
|
|
57
|
+
"Gtk",
|
|
58
|
+
"Adw"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@girs/gio-2.0": "4.6.0",
|
|
63
|
+
"@girs/gjs": "4.6.0",
|
|
64
|
+
"@girs/glib-2.0": "4.6.0",
|
|
65
|
+
"@girs/gobject-2.0": "4.6.0",
|
|
66
|
+
"@girs/gtk-4.0": "4.6.0"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"effect": "4.0.0-rc.112"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@gjsify/cli": "^0.48.0",
|
|
73
|
+
"@gjsify/unit": "^0.48.0",
|
|
74
|
+
"@types/node": "^25.9.2",
|
|
75
|
+
"effect": "4.0.0-rc.112",
|
|
76
|
+
"typescript": "^6.0.3"
|
|
77
|
+
},
|
|
78
|
+
"author": "Pascal Garber <pascal@artandcode.studio>",
|
|
79
|
+
"license": "MIT",
|
|
80
|
+
"repository": {
|
|
81
|
+
"type": "git",
|
|
82
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
83
|
+
"directory": "packages/framework/effect-platform"
|
|
84
|
+
},
|
|
85
|
+
"bugs": {
|
|
86
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
87
|
+
},
|
|
88
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/framework/effect-platform#readme"
|
|
89
|
+
}
|