@gjsify/http-soup-bridge 0.2.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 +96 -0
- package/lib/esm/gjsifyhttpsoupbridge-1.0.d.js +0 -0
- package/lib/esm/index.js +9 -0
- package/lib/types/index.d.ts +7 -0
- package/meson.build +39 -0
- package/package.json +60 -0
- package/prebuilds/linux-aarch64/.gitkeep +0 -0
- package/prebuilds/linux-aarch64/GjsifyHttpSoupBridge-1.0.gir +533 -0
- package/prebuilds/linux-aarch64/GjsifyHttpSoupBridge-1.0.typelib +0 -0
- package/prebuilds/linux-aarch64/libgjsifyhttpsoupbridge.so +0 -0
- package/prebuilds/linux-ppc64/GjsifyHttpSoupBridge-1.0.gir +533 -0
- package/prebuilds/linux-ppc64/GjsifyHttpSoupBridge-1.0.typelib +0 -0
- package/prebuilds/linux-ppc64/libgjsifyhttpsoupbridge.so +0 -0
- package/prebuilds/linux-riscv64/GjsifyHttpSoupBridge-1.0.gir +532 -0
- package/prebuilds/linux-riscv64/GjsifyHttpSoupBridge-1.0.typelib +0 -0
- package/prebuilds/linux-riscv64/libgjsifyhttpsoupbridge.so +0 -0
- package/prebuilds/linux-s390x/GjsifyHttpSoupBridge-1.0.gir +533 -0
- package/prebuilds/linux-s390x/GjsifyHttpSoupBridge-1.0.typelib +0 -0
- package/prebuilds/linux-s390x/libgjsifyhttpsoupbridge.so +0 -0
- package/prebuilds/linux-x86_64/.gitkeep +0 -0
- package/prebuilds/linux-x86_64/GjsifyHttpSoupBridge-1.0.gir +533 -0
- package/prebuilds/linux-x86_64/GjsifyHttpSoupBridge-1.0.typelib +0 -0
- package/prebuilds/linux-x86_64/libgjsifyhttpsoupbridge.so +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @gjsify/http-soup-bridge
|
|
2
|
+
|
|
3
|
+
Vala-based main-thread bridge for libsoup HTTP server. Used by
|
|
4
|
+
[`@gjsify/http`](../http/) to keep MCP / SSE / long-poll workloads stable on
|
|
5
|
+
GJS.
|
|
6
|
+
|
|
7
|
+
## Why this package exists
|
|
8
|
+
|
|
9
|
+
`@gjsify/http` is built on top of `Soup.Server` / `Soup.ServerMessage`. Two
|
|
10
|
+
GJS↔libsoup binding races make the server crash silently with
|
|
11
|
+
`gjs exited with code null` for any non-trivial workload that involves a
|
|
12
|
+
non-GJS HTTP client (MCP Inspector subprocess, browser EventSource, raw
|
|
13
|
+
`node -e fetch`):
|
|
14
|
+
|
|
15
|
+
1. **Boxed-Source GC race.** A JS wrapper around a libsoup-internal
|
|
16
|
+
`GLib.Source` survives past the underlying source's lifetime. SpiderMonkey
|
|
17
|
+
GC eventually finalises it (`g_timeout_add_seconds(10, …)` in
|
|
18
|
+
`GjsContextPrivate::trigger_gc_if_needed`), the finalizer calls
|
|
19
|
+
`g_source_unref` on a freed source, and `g_source_unref_internal:
|
|
20
|
+
assertion 'old_ref > 0' failed` is followed immediately by SIGSEGV.
|
|
21
|
+
|
|
22
|
+
2. **Shared-`GMainContext` ref imbalance.** The thread-default `GMainContext`
|
|
23
|
+
is shared between libsoup's `SoupMessageIOHTTP1.async_context`
|
|
24
|
+
(`refs/libsoup/libsoup/server/http1/soup-server-message-io-http1.c:70,87`)
|
|
25
|
+
and GJS's own `MainLoop::spin` (`refs/gjs/gjs/mainloop.cpp:28-29`). The
|
|
26
|
+
libsoup C-side ref/unref pairs are correct; the imbalance comes from the
|
|
27
|
+
GJS binding layer, where some Boxed wrapper retains a ref past the
|
|
28
|
+
underlying object's lifetime. Surfaces as
|
|
29
|
+
`g_main_context_unref: assertion 'g_atomic_int_get (&context->ref_count)
|
|
30
|
+
> 0' failed`.
|
|
31
|
+
|
|
32
|
+
Both crashes have the same shape: a JS-visible refcounted libsoup boxed (or
|
|
33
|
+
something containing one) ends up at refcount zero on the C side while a JS
|
|
34
|
+
finalizer still believes it owns a reference.
|
|
35
|
+
|
|
36
|
+
The solution is to keep every libsoup boxed in C-space. This package
|
|
37
|
+
exposes three thin GObject classes from Vala — `Server`, `Request`,
|
|
38
|
+
`Response` — that own the underlying `Soup.Server` / `Soup.ServerMessage`
|
|
39
|
+
privately. JS callers only see the bridge classes plus signals dispatched
|
|
40
|
+
through `GLib.Idle.add()` to the main context, so SpiderMonkey GC can never
|
|
41
|
+
race a libsoup-side cleanup.
|
|
42
|
+
|
|
43
|
+
The same pattern is used in
|
|
44
|
+
[`@gjsify/webrtc-native`](../../web/webrtc-native/) for the equivalent
|
|
45
|
+
GstWebRTC threading problem.
|
|
46
|
+
|
|
47
|
+
## Implementation outline
|
|
48
|
+
|
|
49
|
+
* `src/vala/server.vala` — `Server` class wraps `Soup.Server`, emits
|
|
50
|
+
`request_received(req, res)` / `upgrade(req, iostream, head)` signals.
|
|
51
|
+
* `src/vala/request.vala` — `Request` class wraps `Soup.ServerMessage`'s
|
|
52
|
+
read side: method, url, headers, body, peer-close detection.
|
|
53
|
+
* `src/vala/response.vala` — `Response` class wraps the write side:
|
|
54
|
+
`set_header`, `write_head`, `write_chunk`, `end`, plus the pause/unpause
|
|
55
|
+
bookkeeping that previously lived in `@gjsify/http`'s
|
|
56
|
+
`SoupMessageLifecycle.ts`.
|
|
57
|
+
* `src/vala/peer-close-watch.vala` — C-side helper that does
|
|
58
|
+
`g_socket_create_source(IN | HUP | ERR)` + non-blocking
|
|
59
|
+
`g_socket_receive(MSG_PEEK, 1)` to detect peer half-close on paused
|
|
60
|
+
long-poll messages. (This is the capability we couldn't get from JS:
|
|
61
|
+
`Gio.Socket.condition_check` doesn't expose `POLLRDHUP` and
|
|
62
|
+
`Gio.Socket.receive_message(MSG_PEEK)` is not introspectable.)
|
|
63
|
+
|
|
64
|
+
## Build
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
yarn workspace @gjsify/http-soup-bridge run init:meson
|
|
68
|
+
yarn workspace @gjsify/http-soup-bridge run build:meson
|
|
69
|
+
yarn workspace @gjsify/http-soup-bridge run build:prebuilds
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This produces `prebuilds/linux-x86_64/{libgjsifyhttpsoupbridge.so,
|
|
73
|
+
GjsifyHttpSoupBridge-1.0.gir, GjsifyHttpSoupBridge-1.0.typelib}`. The
|
|
74
|
+
`@gjsify/cli` runtime injects `LD_LIBRARY_PATH` and `GI_TYPELIB_PATH` from
|
|
75
|
+
the `gjsify.prebuilds` field at startup, so consumers don't need to install
|
|
76
|
+
the bridge into a system path.
|
|
77
|
+
|
|
78
|
+
The CI workflow at `.github/workflows/prebuilds.yml` builds prebuilds for
|
|
79
|
+
linux-x86_64, linux-aarch64, linux-ppc64, linux-s390x, and linux-riscv64
|
|
80
|
+
and auto-commits them to the repo. x86_64 and aarch64 use native GitHub
|
|
81
|
+
runners; ppc64, s390x, and riscv64 use QEMU via `uraimo/run-on-arch-action`.
|
|
82
|
+
|
|
83
|
+
## TS types
|
|
84
|
+
|
|
85
|
+
Until `@girs/gjsifyhttpsoupbridge-1.0` is published to npm, types are
|
|
86
|
+
generated locally from the freshly-built GIR via `ts-for-gir`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx @ts-for-gir/cli generate \
|
|
90
|
+
--package --npmScope=@girs --outdir=node_modules \
|
|
91
|
+
--girDirectories=packages/node/http-soup-bridge/build \
|
|
92
|
+
GjsifyHttpSoupBridge-1.0
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
After publication this step disappears — the package's `dependencies`
|
|
96
|
+
already references `@girs/gjsifyhttpsoupbridge-1.0`.
|
|
File without changes
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import GjsifyHttpSoupBridge from "gi://GjsifyHttpSoupBridge?version=1.0";
|
|
2
|
+
const Server = GjsifyHttpSoupBridge.Server;
|
|
3
|
+
const Request = GjsifyHttpSoupBridge.Request;
|
|
4
|
+
const Response = GjsifyHttpSoupBridge.Response;
|
|
5
|
+
export {
|
|
6
|
+
Request,
|
|
7
|
+
Response,
|
|
8
|
+
Server
|
|
9
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import GjsifyHttpSoupBridge from 'gi://GjsifyHttpSoupBridge?version=1.0';
|
|
2
|
+
export declare const Server: typeof GjsifyHttpSoupBridge.Server;
|
|
3
|
+
export type Server = GjsifyHttpSoupBridge.Server;
|
|
4
|
+
export declare const Request: typeof GjsifyHttpSoupBridge.Request;
|
|
5
|
+
export type Request = GjsifyHttpSoupBridge.Request;
|
|
6
|
+
export declare const Response: typeof GjsifyHttpSoupBridge.Response;
|
|
7
|
+
export type Response = GjsifyHttpSoupBridge.Response;
|
package/meson.build
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
project('GjsifyHttpSoupBridge', ['c', 'vala'], version: '1.0')
|
|
2
|
+
|
|
3
|
+
root_dir = meson.current_source_dir()
|
|
4
|
+
|
|
5
|
+
dependencies = [
|
|
6
|
+
dependency('glib-2.0'),
|
|
7
|
+
dependency('gobject-2.0'),
|
|
8
|
+
dependency('gio-2.0'),
|
|
9
|
+
dependency('libsoup-3.0', version: '>=3.6'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
sources = files(
|
|
13
|
+
'src/vala/peer-close-watch.vala',
|
|
14
|
+
'src/vala/response.vala',
|
|
15
|
+
'src/vala/request.vala',
|
|
16
|
+
'src/vala/server.vala',
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
libGjsifyHttpSoupBridge = library('gjsifyhttpsoupbridge', sources,
|
|
20
|
+
dependencies: dependencies,
|
|
21
|
+
vala_gir: meson.project_name() + '-1.0.gir',
|
|
22
|
+
install: true,
|
|
23
|
+
install_dir: [true, true, true, true],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
g_ir_compiler = find_program('g-ir-compiler')
|
|
27
|
+
|
|
28
|
+
custom_target(meson.project_name() + '-1.0.typelib',
|
|
29
|
+
command: [
|
|
30
|
+
g_ir_compiler,
|
|
31
|
+
'--shared-library', 'libgjsifyhttpsoupbridge.so',
|
|
32
|
+
'--output', '@OUTPUT@',
|
|
33
|
+
meson.current_build_dir() / meson.project_name() + '-1.0.gir',
|
|
34
|
+
],
|
|
35
|
+
output: meson.project_name() + '-1.0.typelib',
|
|
36
|
+
depends: libGjsifyHttpSoupBridge,
|
|
37
|
+
install: true,
|
|
38
|
+
install_dir: get_option('libdir') / 'girepository-1.0',
|
|
39
|
+
)
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/http-soup-bridge",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Vala-based main-thread bridge for libsoup HTTP server. Marshals SoupServer + SoupServerMessage signals onto the GLib main context and keeps every libsoup boxed type (MessageBody, MessageHeaders, the message's GMainContext ref, the HTTP1 IO GSource) on the C side, so SpiderMonkey GC has no chance to race a libsoup-side cleanup. Used by @gjsify/http to keep MCP/SSE/long-poll workloads stable on GJS.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/esm/index.js",
|
|
7
|
+
"module": "lib/esm/index.js",
|
|
8
|
+
"types": "lib/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/types/index.d.ts",
|
|
12
|
+
"default": "./lib/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"prebuilds",
|
|
18
|
+
"meson.build",
|
|
19
|
+
"src/vala"
|
|
20
|
+
],
|
|
21
|
+
"gjsify": {
|
|
22
|
+
"prebuilds": "prebuilds"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clear": "rm -rf lib build tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo || exit 0",
|
|
26
|
+
"check": "tsc --noEmit",
|
|
27
|
+
"init:meson": "meson setup build .",
|
|
28
|
+
"init:meson:wipe": "yarn init:meson --wipe",
|
|
29
|
+
"build": "yarn build:gjsify && yarn build:types",
|
|
30
|
+
"build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}'",
|
|
31
|
+
"build:meson": "yarn init:meson && meson compile -C build",
|
|
32
|
+
"build:types": "tsc",
|
|
33
|
+
"build:gir-types": "ts-for-gir generate --externalDeps --allowMissingDeps --girDirectories=./prebuilds/linux-x86_64 --girDirectories=/usr/share/gir-1.0 --modules=GjsifyHttpSoupBridge-1.0 --outdir=src/ts --npmScope=@girs --package=false --ignoreVersionConflicts=true",
|
|
34
|
+
"build:prebuilds": "yarn build:meson && mkdir -p prebuilds/linux-x86_64 && cp build/libgjsifyhttpsoupbridge.so build/GjsifyHttpSoupBridge-1.0.gir build/GjsifyHttpSoupBridge-1.0.typelib prebuilds/linux-x86_64/"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"gjs",
|
|
38
|
+
"http",
|
|
39
|
+
"server",
|
|
40
|
+
"libsoup",
|
|
41
|
+
"soup",
|
|
42
|
+
"vala",
|
|
43
|
+
"native",
|
|
44
|
+
"sse",
|
|
45
|
+
"long-poll"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@girs/gio-2.0": "^2.88.0-4.0.0-rc.9",
|
|
49
|
+
"@girs/gjs": "^4.0.0-rc.9",
|
|
50
|
+
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
|
|
51
|
+
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.9",
|
|
52
|
+
"@girs/soup-3.0": "^3.6.6-4.0.0-rc.9"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@gjsify/cli": "^0.2.0",
|
|
56
|
+
"@ts-for-gir/cli": "^4.0.0-rc.9",
|
|
57
|
+
"@types/node": "^25.6.0",
|
|
58
|
+
"typescript": "^6.0.3"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
File without changes
|