@n1h/sweetview 0.0.0-stage → 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE-0BSD +12 -0
  3. package/README.md +268 -3
  4. package/docs/README.md +26 -0
  5. package/docs/app.md +523 -0
  6. package/docs/build.md +169 -0
  7. package/docs/ipc-ws.md +202 -0
  8. package/docs/ipc.md +323 -0
  9. package/docs/native.md +1066 -0
  10. package/docs/page.md +71 -0
  11. package/docs/settings-store.md +125 -0
  12. package/docs/webview-library.md +104 -0
  13. package/docs/window-chrome.md +221 -0
  14. package/docs/worker.md +72 -0
  15. package/file-imports.d.ts +19 -0
  16. package/package.json +66 -4
  17. package/src/app.ts +350 -0
  18. package/src/build.ts +118 -0
  19. package/src/html.ts +71 -0
  20. package/src/index.ts +8 -0
  21. package/src/ipc/bridge.ts +41 -0
  22. package/src/ipc/frame.ts +16 -0
  23. package/src/ipc/index.ts +16 -0
  24. package/src/ipc/memory.ts +33 -0
  25. package/src/ipc/service.ts +75 -0
  26. package/src/ipc-ws/client.ts +111 -0
  27. package/src/ipc-ws/index.ts +5 -0
  28. package/src/ipc-ws/server.ts +133 -0
  29. package/src/make-app.ts +93 -0
  30. package/src/messages.ts +16 -0
  31. package/src/native/app-icon.ts +23 -0
  32. package/src/native/app-menu.ts +18 -0
  33. package/src/native/darwin/app-icon.ts +10 -0
  34. package/src/native/darwin/app-menu.ts +88 -0
  35. package/src/native/darwin/folder-picker.ts +29 -0
  36. package/src/native/darwin/image.ts +47 -0
  37. package/src/native/darwin/menu.ts +89 -0
  38. package/src/native/darwin/notifier.ts +95 -0
  39. package/src/native/darwin/objc.ts +246 -0
  40. package/src/native/darwin/overlay.ts +352 -0
  41. package/src/native/darwin/tray.ts +129 -0
  42. package/src/native/darwin/window.ts +64 -0
  43. package/src/native/folder-picker.ts +15 -0
  44. package/src/native/hit-regions.ts +162 -0
  45. package/src/native/icon.ts +530 -0
  46. package/src/native/index.ts +38 -0
  47. package/src/native/menu.ts +43 -0
  48. package/src/native/notifications.ts +40 -0
  49. package/src/native/overlay.ts +182 -0
  50. package/src/native/platform.ts +25 -0
  51. package/src/native/tray.ts +41 -0
  52. package/src/native/win32/api.ts +234 -0
  53. package/src/native/win32/app-icon.ts +5 -0
  54. package/src/native/win32/app-menu.ts +4 -0
  55. package/src/native/win32/folder-picker.ts +121 -0
  56. package/src/native/win32/icon.ts +85 -0
  57. package/src/native/win32/menu.ts +107 -0
  58. package/src/native/win32/message-window.ts +72 -0
  59. package/src/native/win32/notifier.ts +28 -0
  60. package/src/native/win32/overlay.ts +499 -0
  61. package/src/native/win32/tray.ts +228 -0
  62. package/src/native/win32/window.ts +77 -0
  63. package/src/native/window.ts +40 -0
  64. package/src/page.ts +48 -0
  65. package/src/ring.ts +110 -0
  66. package/src/settings-store.ts +101 -0
  67. package/src/webview-lib.ts +60 -0
  68. package/src/window-chrome.ts +118 -0
  69. package/src/worker.ts +72 -0
package/docs/ipc.md ADDED
@@ -0,0 +1,323 @@
1
+ # @n1h/sweetview/ipc
2
+
3
+ The typed contract between a page and its backend: a `Service` on the backend, a `Bridge` on the page, and the JSON frames a host's transport carries between them. The module imports nothing outside itself and uses no runtime API, so it runs in a browser page, a webview page, a Worker, or Bun. Hosts supply the transport; [`ipc-ws.md`](ipc-ws.md) covers the WebSocket one, and [`../README.md`](../README.md) covers install and the window host.
4
+
5
+ ```ts
6
+ import { type CallFrame, type EventFrame, type Frame, type ReplyFrame, defineService, type AnyEvent, type Ctx, type Emit, type EventMap, type Handlers, type MethodMap, type Service, type ServiceDefinition, type Session, type SessionKind, createBridge, type Bridge, type Transport, type TransportKind, memoryTransport } from "@n1h/sweetview/ipc";
7
+ ```
8
+
9
+ ## Reference
10
+
11
+ ### CallFrame
12
+
13
+ ```ts
14
+ export type CallFrame = { kind: "call"; seq: string | null; method: string; params: unknown };
15
+ ```
16
+
17
+ A request from the page to the backend.
18
+
19
+ | Field | Type | Meaning |
20
+ |---|---|---|
21
+ | `kind` | `"call"` | Frame discriminant. |
22
+ | `seq` | `string \| null` | Correlation id chosen by the sender. `null` means fire and forget: no reply frame is sent. |
23
+ | `method` | `string` | Method name in the service's `MethodMap`. |
24
+ | `params` | `unknown` | The single argument. A method called without an argument carries `null`. |
25
+
26
+ ### EventFrame
27
+
28
+ ```ts
29
+ export type EventFrame = { kind: "event"; name: string; payload: unknown };
30
+ ```
31
+
32
+ An unsolicited message from the backend to the page.
33
+
34
+ | Field | Type | Meaning |
35
+ |---|---|---|
36
+ | `kind` | `"event"` | Frame discriminant. |
37
+ | `name` | `string` | Event name in the service's `EventMap`. |
38
+ | `payload` | `unknown` | The event payload. |
39
+
40
+ ### Frame
41
+
42
+ ```ts
43
+ export type Frame = CallFrame | ReplyFrame | EventFrame;
44
+ ```
45
+
46
+ Every frame a transport carries, as JSON. A service never sees frames. The host that wraps a service turns each `CallFrame` into a `Service.call` and answers with a `ReplyFrame`. The page's transport matches each `ReplyFrame` to its call by `seq` and ignores a `ReplyFrame` whose `seq` matches no pending call.
47
+
48
+ ### ReplyFrame
49
+
50
+ ```ts
51
+ export type ReplyFrame =
52
+ | { kind: "reply"; seq: string; ok: true; result: unknown }
53
+ | { kind: "reply"; seq: string; ok: false; error: string };
54
+ ```
55
+
56
+ The backend's answer to one `CallFrame` whose `seq` was not `null`.
57
+
58
+ | Field | Type | Meaning |
59
+ |---|---|---|
60
+ | `kind` | `"reply"` | Frame discriminant. |
61
+ | `seq` | `string` | The `seq` of the call being answered. |
62
+ | `ok` | `boolean` | `true` when `Service.call` resolved. `false` when it rejected, including for an unknown method. |
63
+ | `result` | `unknown` | The handler's return value. Present when `ok` is `true`. |
64
+ | `error` | `string` | The rejection's `message`, or `String(value)` when the rejection is not an `Error`. Present when `ok` is `false`. The page sees it, so it must be safe for the user to see. |
65
+
66
+ ### defineService
67
+
68
+ ```ts
69
+ export function defineService<M extends MethodMap, E extends EventMap>(setup: (emit: Emit<E>) => ServiceDefinition<M>): Service<M, E>;
70
+ ```
71
+
72
+ Builds a `Service` from a handler table. `setup` runs once, synchronously, inside `defineService`. It receives `emit`. Handlers and background work call `emit` to push events.
73
+
74
+ | Field | Type | Default | Meaning |
75
+ |---|---|---|---|
76
+ | `setup` | `(emit: Emit<E>) => ServiceDefinition<M>` | required | Returns the handlers and the optional lifecycle hooks. |
77
+
78
+ Returns: a `Service<M, E>` whose `call` looks the method up among the own properties of `methods`, so names inherited from `Object.prototype` such as `constructor` are not methods.
79
+
80
+ Throws: whatever `setup` throws, synchronously.
81
+
82
+ The returned service behaves as follows.
83
+
84
+ - `call` awaits the handler and resolves with its result.
85
+ - `call` rejects with `Error("unknown method <name>")` when `methods` has no own property of that name.
86
+ - `call` rejects with whatever the handler throws.
87
+ - `onEvent` listeners run synchronously, in subscription order, each time `emit` is called.
88
+ - A listener that throws stops `emit`. Later listeners do not run, and the exception propagates to the caller of `emit`.
89
+ - `endSession` calls the definition's `endSession`, when present.
90
+ - `dispose` awaits the definition's `dispose`, when present, then removes every `onEvent` listener. Events emitted while the definition's `dispose` runs still reach listeners.
91
+ - `dispose` removes the listeners even when the definition's `dispose` throws or rejects, and then rejects with that error.
92
+ - `dispose` does not end sessions and does not wait for or cancel calls in flight. A disposed service still runs `call` and still accepts `onEvent` subscriptions.
93
+
94
+ Handlers receive params exactly as the host decoded them. Hosts do not validate params, so a handler that needs a shape checks it.
95
+
96
+ ### AnyEvent
97
+
98
+ ```ts
99
+ export type AnyEvent<E extends EventMap> = { [N in keyof E & string]: { name: N; payload: E[N] } }[keyof E & string];
100
+ ```
101
+
102
+ One event of `E` as a discriminated union of `{ name, payload }`. A listener that switches on `name` gets the matching `payload` type.
103
+
104
+ ### Ctx
105
+
106
+ ```ts
107
+ export type Ctx = { session: Session };
108
+ ```
109
+
110
+ The second argument of every handler.
111
+
112
+ | Field | Type | Meaning |
113
+ |---|---|---|
114
+ | `session` | `Session` | The client that made the call. |
115
+
116
+ ### Emit
117
+
118
+ ```ts
119
+ export type Emit<E extends EventMap> = <N extends keyof E & string>(name: N, payload: E[N], to?: string) => void;
120
+ ```
121
+
122
+ Calls every `onEvent` listener synchronously with `name`, `payload` and `to`. Hosts and `memoryTransport` deliver an event without `to` to every session, and an event with `to` only to the session whose `id` equals `to`. An event targeted at a session that no longer exists is dropped. An event emitted while nothing is subscribed is lost. Nothing is buffered.
123
+
124
+ | Field | Type | Default | Meaning |
125
+ |---|---|---|---|
126
+ | `name` | `N` | required | Event name. |
127
+ | `payload` | `E[N]` | required | Event payload. It must survive JSON encoding to cross a real transport. |
128
+ | `to` | `string` | none | Session id to target. |
129
+
130
+ ### EventMap
131
+
132
+ ```ts
133
+ export type EventMap = Record<string, unknown>;
134
+ ```
135
+
136
+ Maps each event name to its payload type. It is shared by both sides.
137
+
138
+ ### Handlers
139
+
140
+ ```ts
141
+ export type Handlers<M extends MethodMap> = {
142
+ [K in keyof M & string]: (params: M[K][0], ctx: Ctx) => M[K][1] | Promise<M[K][1]>;
143
+ };
144
+ ```
145
+
146
+ One handler per method of `M`. A handler returns the result or a promise of it. A handler throws to send an error reply. Through a host, the page sees the error's message. Through `memoryTransport`, the page sees the thrown value itself.
147
+
148
+ ### MethodMap
149
+
150
+ ```ts
151
+ export type MethodMap = Record<string, [params: unknown, result: unknown]>;
152
+ ```
153
+
154
+ Maps each method name to a `[params, result]` tuple. It is shared by both sides. A params type of `void` or `undefined` means the method takes no argument. A method has at most one argument, so several values go in one object.
155
+
156
+ ### Service
157
+
158
+ ```ts
159
+ export interface Service<M extends MethodMap, E extends EventMap> {
160
+ call<K extends keyof M & string>(method: K, params: M[K][0], ctx: Ctx): Promise<M[K][1]>;
161
+ onEvent(fn: (name: keyof E & string, payload: E[keyof E], to?: string) => void): () => void;
162
+ endSession(sessionId: string): void;
163
+ dispose(): Promise<void>;
164
+ }
165
+ ```
166
+
167
+ The backend, free of any transport. A host wraps one service and serves it to its sessions. `defineService` builds one.
168
+
169
+ | Field | Type | Meaning |
170
+ |---|---|---|
171
+ | `call` | `(method, params, ctx) => Promise<result>` | Runs one method for the session in `ctx`. |
172
+ | `onEvent` | `(fn) => () => void` | Subscribes to every emitted event, with the target session id in `to` when there is one. Returns an unsubscribe function. |
173
+ | `endSession` | `(sessionId: string) => void` | A host calls this when a client goes away. The service drops anything it held for that session. |
174
+ | `dispose` | `() => Promise<void>` | The process is shutting down. Hosts do not call this; the application does. |
175
+
176
+ ### ServiceDefinition
177
+
178
+ ```ts
179
+ export type ServiceDefinition<M extends MethodMap> = {
180
+ methods: Handlers<M>;
181
+ endSession?: (sessionId: string) => void;
182
+ dispose?: () => void | Promise<void>;
183
+ };
184
+ ```
185
+
186
+ What `setup` returns to `defineService`.
187
+
188
+ | Field | Type | Default | Meaning |
189
+ |---|---|---|---|
190
+ | `methods` | `Handlers<M>` | required | One handler per method. |
191
+ | `endSession` | `(sessionId: string) => void` | none | Called when a session ends. |
192
+ | `dispose` | `() => void \| Promise<void>` | none | Called, and awaited, by `Service.dispose`. |
193
+
194
+ ### Session
195
+
196
+ ```ts
197
+ export type Session = { id: string; kind: SessionKind };
198
+ ```
199
+
200
+ One connected client, as the host identifies it.
201
+
202
+ | Field | Type | Meaning |
203
+ |---|---|---|
204
+ | `id` | `string` | Unique within the host. The host assigns it; the page never sends it. |
205
+ | `kind` | `SessionKind` | Which kind of host the client came through. |
206
+
207
+ ### SessionKind
208
+
209
+ ```ts
210
+ export type SessionKind = "webview" | "web";
211
+ ```
212
+
213
+ `"webview"` for a page in a sweetview window. `"web"` for a page in a browser or for `memoryTransport` by default.
214
+
215
+ ### createBridge
216
+
217
+ ```ts
218
+ export function createBridge<M extends MethodMap, E extends EventMap>(transport: Transport): Bridge<M, E>;
219
+ ```
220
+
221
+ Puts the typed `Bridge` surface on a `Transport`. It subscribes to the transport's events once and fans them out by name.
222
+
223
+ | Field | Type | Default | Meaning |
224
+ |---|---|---|---|
225
+ | `transport` | `Transport` | required | The host's transport. |
226
+
227
+ Returns: a `Bridge<M, E>` with the transport's `kind`.
228
+
229
+ Throws: whatever `transport.onEvent` throws, synchronously.
230
+
231
+ The bridge holds its transport subscription for as long as the transport lives. It has no method to release it.
232
+
233
+ ### Bridge
234
+
235
+ ```ts
236
+ export interface Bridge<M extends MethodMap, E extends EventMap> {
237
+ readonly kind: TransportKind;
238
+ rpc<K extends keyof M & string>(method: K, ...params: M[K][0] extends void ? [] : [M[K][0]]): Promise<M[K][1]>;
239
+ notify<K extends keyof M & string>(method: K, ...params: M[K][0] extends void ? [] : [M[K][0]]): void;
240
+ on<N extends keyof E & string>(name: N, fn: (payload: E[N]) => void): () => void;
241
+ }
242
+ ```
243
+
244
+ The page's typed view of the service. A method whose params type is `void` or `undefined` takes no argument. The bridge sends `null` as params when the argument is absent or `undefined`.
245
+
246
+ | Field | Type | Meaning |
247
+ |---|---|---|
248
+ | `kind` | `TransportKind` | The transport's kind, so a page can tell a window from a browser tab. |
249
+ | `rpc` | `(method, params?) => Promise<result>` | Calls a method. Resolves with its result. Rejects with whatever `transport.call` rejects with: through a host, an `Error` carrying the backend's message or the transport's own error. |
250
+ | `notify` | `(method, params?) => void` | Calls a method without waiting. No reply comes back and failures are not reported to the caller. |
251
+ | `on` | `(name, fn) => () => void` | Runs `fn` with the payload of each event called `name`. Returns an unsubscribe function. Events that arrive while no listener is subscribed are dropped. |
252
+
253
+ ### Transport
254
+
255
+ ```ts
256
+ export interface Transport {
257
+ readonly kind: TransportKind;
258
+ call(method: string, params: unknown): Promise<unknown>;
259
+ notify(method: string, params: unknown): void;
260
+ onEvent(fn: (name: string, payload: unknown) => void): () => void;
261
+ }
262
+ ```
263
+
264
+ The one thing a host provides to the page. `@n1h/sweetview/page` provides one for a sweetview window, `@n1h/sweetview/ipc-ws/client` one for a browser, and `memoryTransport` one in the same process.
265
+
266
+ | Field | Type | Meaning |
267
+ |---|---|---|
268
+ | `kind` | `TransportKind` | Which host this transport reaches. |
269
+ | `call` | `(method: string, params: unknown) => Promise<unknown>` | Resolves with the result. Rejects with an `Error` carrying the backend's message. |
270
+ | `notify` | `(method: string, params: unknown) => void` | Fire and forget: no reply. |
271
+ | `onEvent` | `(fn) => () => void` | Receives every event addressed to this client. Returns an unsubscribe function. |
272
+
273
+ ### TransportKind
274
+
275
+ ```ts
276
+ export type TransportKind = "webview" | "web";
277
+ ```
278
+
279
+ The same values as `SessionKind`, seen from the page.
280
+
281
+ ### memoryTransport
282
+
283
+ ```ts
284
+ export function memoryTransport<M extends MethodMap, E extends EventMap>(
285
+ service: Service<M, E>,
286
+ session: Session = { id: `memory-${nextId++}`, kind: "web" },
287
+ ): Transport & { close(): void };
288
+ ```
289
+
290
+ A `Transport` straight onto a `Service` in the same process, with no frames and no host. It is meant for tests and for rendering a frontend in plain Bun.
291
+
292
+ | Field | Type | Default | Meaning |
293
+ |---|---|---|---|
294
+ | `service` | `Service<M, E>` | required | The service to call. |
295
+ | `session` | `Session` | `{ id: "memory-<n>", kind: "web" }` | The session every call runs as. `<n>` counts from 1 across the memory transports created without a `session`. |
296
+
297
+ Returns: a `Transport` with `kind` set to `session.kind`, plus `close()`.
298
+
299
+ - `call` passes params and results by reference. Nothing is JSON-encoded, so values that would not survive a real transport still arrive intact.
300
+ - `call` rejects with whatever `Service.call` rejects with.
301
+ - `notify` discards both the result and any rejection.
302
+ - Events reach this transport when they are broadcast or targeted at `session.id`. They are delivered synchronously inside `emit`, so a listener on this transport that throws makes the backend's `emit` throw.
303
+ - `close()` stops event delivery and calls `service.endSession(session.id)`. Each further `close()` calls `service.endSession(session.id)` again. Calls made after `close()` still reach the service.
304
+
305
+ ## Example
306
+
307
+ ```ts
308
+ import { createBridge, defineService, memoryTransport } from "@n1h/sweetview/ipc";
309
+
310
+ type Methods = { add: [{ a: number; b: number }, number] };
311
+ type Events = { added: { sum: number } };
312
+ const service = defineService<Methods, Events>((emit) => ({
313
+ methods: {
314
+ add: ({ a, b }, ctx) => {
315
+ emit("added", { sum: a + b }, ctx.session.id);
316
+ return a + b;
317
+ },
318
+ },
319
+ }));
320
+ const bridge = createBridge<Methods, Events>(memoryTransport(service));
321
+ bridge.on("added", ({ sum }) => console.log("event", sum));
322
+ console.log("result", await bridge.rpc("add", { a: 2, b: 3 }));
323
+ ```