plusui-native-core 0.1.66 → 0.1.68

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.
@@ -1,272 +1,190 @@
1
- export type ConnectionKind =
2
- | "call"
3
- | "fire"
4
- | "result"
5
- | "event"
6
- | "stream"
7
- | "sub"
8
- | "unsub"
9
- | "publish"
10
- | "error";
11
-
12
- export type ConnectionEnvelope = {
13
- kind: ConnectionKind;
14
- id?: string;
15
- name: string;
16
- payload?: unknown;
17
- error?: string;
18
- };
19
-
20
- type MessageCallback = (payload: any) => void;
21
-
22
- class ConnectionClient {
23
- private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
24
- private listeners = new Map<string, Set<MessageCallback>>();
25
-
26
- constructor() {
27
- const host = globalThis as any;
28
-
29
- host.__plusuiConnectionMessage = (message: unknown) => {
30
- this.handleIncoming(message);
31
- };
32
-
33
- if (typeof window !== "undefined") {
34
- window.addEventListener("plusui:connection:message", (ev: Event) => {
35
- const custom = ev as CustomEvent<unknown>;
36
- this.handleIncoming(custom.detail);
37
- });
38
- }
39
- }
40
-
41
- private nextId(): string {
42
- return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
43
- }
44
-
45
- private async send(env: ConnectionEnvelope): Promise<any> {
46
- const host = globalThis as any;
47
-
48
- if (typeof host.__invoke__ === "function") {
49
- return host.__invoke__("connection.dispatch", env);
50
- }
51
-
52
- if (host.ipc?.postMessage) {
53
- host.ipc.postMessage(JSON.stringify(env));
54
- return null;
55
- }
56
-
57
- return null;
58
- }
59
-
60
- private decode(message: unknown): ConnectionEnvelope | null {
61
- if (!message) {
62
- return null;
63
- }
64
- if (typeof message === "string") {
65
- try {
66
- return JSON.parse(message) as ConnectionEnvelope;
67
- } catch {
68
- return null;
69
- }
70
- }
71
- if (typeof message === "object") {
72
- return message as ConnectionEnvelope;
73
- }
74
- return null;
75
- }
76
-
77
- private handleIncoming(message: unknown): void {
78
- const env = this.decode(message);
79
- if (!env) {
80
- return;
81
- }
82
-
83
- if ((env.kind === "result" || env.kind === "error") && env.id) {
84
- const entry = this.pending.get(env.id);
85
- if (!entry) {
86
- return;
87
- }
88
- this.pending.delete(env.id);
89
- if (env.kind === "error") {
90
- entry.reject(new Error(env.error || "Connection call failed"));
91
- } else {
92
- entry.resolve(env.payload);
93
- }
94
- return;
95
- }
96
-
97
- if (env.kind === "event" || env.kind === "stream" || env.kind === "publish") {
98
- const handlers = this.listeners.get(env.name);
99
- if (!handlers) {
100
- return;
101
- }
102
- for (const handler of handlers) {
103
- handler(env.payload);
104
- }
105
- }
106
- }
107
-
108
- async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
109
- const id = this.nextId();
110
- const promise = new Promise<TOut>((resolve, reject) => {
111
- this.pending.set(id, { resolve, reject });
112
- });
113
-
114
- await this.send({ kind: "call", id, name, payload });
115
- return promise;
116
- }
117
-
118
- fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
119
- void this.send({ kind: "fire", name, payload });
120
- }
121
-
122
- on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
123
- const existing = this.listeners.get(name) ?? new Set<MessageCallback>();
124
- existing.add(callback as MessageCallback);
125
- this.listeners.set(name, existing);
126
-
127
- return () => {
128
- const current = this.listeners.get(name);
129
- if (!current) {
130
- return;
131
- }
132
- current.delete(callback as MessageCallback);
133
- if (current.size === 0) {
134
- this.listeners.delete(name);
135
- }
136
- };
137
- }
138
-
139
- stream<TData = unknown>(name: string) {
140
- return {
141
- subscribe: (callback: (payload: TData) => void): (() => void) => {
142
- void this.send({ kind: "sub", name });
143
- const off = this.on<TData>(name, callback);
144
- return () => {
145
- off();
146
- void this.send({ kind: "unsub", name });
147
- };
148
- },
149
- };
150
- }
151
-
152
- channel<TData = unknown>(name: string) {
153
- return {
154
- subscribe: (callback: (payload: TData) => void): (() => void) => {
155
- void this.send({ kind: "sub", name });
156
- const off = this.on<TData>(name, callback);
157
- return () => {
158
- off();
159
- void this.send({ kind: "unsub", name });
160
- };
161
- },
162
- publish: (payload: TData): void => {
163
- void this.send({ kind: "publish", name, payload });
164
- },
165
- };
166
- }
167
- }
168
-
169
- // Create the connection client instance
170
- const connectionClient = new ConnectionClient();
171
-
172
- // Simple connection API - just emit() and on()
173
- export const connect = {
174
- // Send a message to backend
175
- emit: <TIn = Record<string, unknown>>(name: string, payload: TIn): void => {
176
- connectionClient.fire(name, payload);
177
- },
178
-
179
- // Listen for messages from backend
180
- on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
181
- return connectionClient.on<TData>(name, callback);
182
- },
183
-
184
- feature: (scope: string): FeatureConnect => {
185
- return createFeatureConnect(scope);
186
- },
187
- };
188
-
189
- // Export the raw client for advanced use cases (call, fire, stream, channel)
190
- export const connection = connectionClient;
191
-
192
- export type FeatureConnect = {
193
- emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
194
- on: <TData = unknown>(name: string, callback: (payload: TData) => void) => (() => void);
195
- call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn) => Promise<TOut>;
196
- stream: <TData = unknown>(name: string) => {
197
- subscribe: (callback: (payload: TData) => void) => (() => void);
198
- };
199
- channel: <TData = unknown>(name: string) => {
200
- subscribe: (callback: (payload: TData) => void) => (() => void);
201
- publish: (payload: TData) => void;
202
- };
203
- scoped: (scope: string) => FeatureConnect;
204
- };
205
-
206
- function scopeName(scope: string, name: string): string {
207
- if (name.startsWith(`${scope}.`)) {
208
- return name;
209
- }
210
- return `${scope}.${name}`;
211
- }
212
-
213
- async function invokeScoped(method: string, payload?: unknown): Promise<unknown> {
214
- const host = globalThis as any;
215
- if (typeof host.__invoke__ !== 'function') {
216
- return undefined;
217
- }
218
-
219
- const args = payload === undefined ? [] : [payload];
220
- return host.__invoke__(method, args);
221
- }
222
-
223
- export function createFeatureConnect(scope: string): FeatureConnect {
224
- return {
225
- emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => {
226
- const scoped = scopeName(scope, name);
227
- void invokeScoped(scoped, payload);
228
- connect.emit(scoped, payload);
229
-
230
- if (typeof window !== 'undefined') {
231
- window.dispatchEvent(new CustomEvent(`plusui:${scoped}`, { detail: payload }));
232
- }
233
- },
234
- on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
235
- const scoped = scopeName(scope, name);
236
- const offConnect = connect.on<TData>(scoped, callback);
237
-
238
- if (typeof window === 'undefined') {
239
- return offConnect;
240
- }
241
-
242
- const domHandler = (event: Event) => {
243
- const custom = event as CustomEvent<TData>;
244
- callback(custom.detail);
245
- };
246
-
247
- window.addEventListener(`plusui:${scoped}`, domHandler as EventListener);
248
-
249
- return () => {
250
- offConnect();
251
- window.removeEventListener(`plusui:${scoped}`, domHandler as EventListener);
252
- };
253
- },
254
- call: <TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> => {
255
- const scoped = scopeName(scope, name);
256
- const host = globalThis as any;
257
-
258
- if (typeof host.__invoke__ === 'function') {
259
- return invokeScoped(scoped, payload) as Promise<TOut>;
260
- }
261
-
262
- return connection.call<TOut, TIn>(scoped, payload);
263
- },
264
- stream: <TData = unknown>(name: string) => {
265
- return connection.stream<TData>(scopeName(scope, name));
266
- },
267
- channel: <TData = unknown>(name: string) => {
268
- return connection.channel<TData>(scopeName(scope, name));
269
- },
270
- scoped: (childScope: string) => createFeatureConnect(scopeName(scope, childScope)),
271
- };
272
- }
1
+ /**
2
+ * PlusUI Connection
3
+ *
4
+ * TWO METHODS. FIVE PRIMITIVES. EVERYTHING YOU NEED.
5
+ *
6
+ * name.on(callback) - Listen for messages
7
+ * name.emit(data) - Send messages
8
+ *
9
+ * Built-in features (win, clipboard, app) are imported from 'plusui'.
10
+ * Custom channels are auto-generated by `plusui connect`.
11
+ */
12
+
13
+ export type ConnectionKind =
14
+ | "call"
15
+ | "fire"
16
+ | "result"
17
+ | "event"
18
+ | "stream"
19
+ | "sub"
20
+ | "unsub"
21
+ | "publish"
22
+ | "error";
23
+
24
+ export type ConnectionEnvelope = {
25
+ kind: ConnectionKind;
26
+ id?: string;
27
+ name: string;
28
+ payload?: unknown;
29
+ error?: string;
30
+ };
31
+
32
+ type MessageCallback = (payload: any) => void;
33
+
34
+ class ConnectionClient {
35
+ private pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void }>();
36
+ private listeners = new Map<string, Set<MessageCallback>>();
37
+
38
+ constructor() {
39
+ const host = globalThis as any;
40
+ host.__plusuiConnectionMessage = (message: unknown) => {
41
+ this.handleIncoming(message);
42
+ };
43
+ if (typeof window !== "undefined") {
44
+ window.addEventListener("plusui:connection:message", (ev: Event) => {
45
+ const custom = ev as CustomEvent<unknown>;
46
+ this.handleIncoming(custom.detail);
47
+ });
48
+ }
49
+ }
50
+
51
+ private nextId(): string {
52
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
53
+ }
54
+
55
+ private async send(env: ConnectionEnvelope): Promise<any> {
56
+ const host = globalThis as any;
57
+ if (typeof host.__invoke__ === "function") {
58
+ return host.__invoke__("connection.dispatch", env);
59
+ }
60
+ if (host.ipc?.postMessage) {
61
+ host.ipc.postMessage(JSON.stringify(env));
62
+ return null;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ private decode(message: unknown): ConnectionEnvelope | null {
68
+ if (!message) return null;
69
+ if (typeof message === "string") {
70
+ try {
71
+ return JSON.parse(message) as ConnectionEnvelope;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ if (typeof message === "object") {
77
+ return message as ConnectionEnvelope;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ private handleIncoming(message: unknown): void {
83
+ const env = this.decode(message);
84
+ if (!env) return;
85
+
86
+ if ((env.kind === "result" || env.kind === "error") && env.id) {
87
+ const entry = this.pending.get(env.id);
88
+ if (!entry) return;
89
+ this.pending.delete(env.id);
90
+ if (env.kind === "error") {
91
+ entry.reject(new Error(env.error || "Connection call failed"));
92
+ } else {
93
+ entry.resolve(env.payload);
94
+ }
95
+ return;
96
+ }
97
+
98
+ if (env.kind === "event" || env.kind === "stream" || env.kind === "publish") {
99
+ const handlers = this.listeners.get(env.name);
100
+ if (!handlers) return;
101
+ for (const handler of handlers) {
102
+ handler(env.payload);
103
+ }
104
+ }
105
+ }
106
+
107
+ async call<TOut = unknown, TIn = Record<string, unknown>>(name: string, payload: TIn): Promise<TOut> {
108
+ const id = this.nextId();
109
+ const promise = new Promise<TOut>((resolve, reject) => {
110
+ this.pending.set(id, { resolve, reject });
111
+ });
112
+ await this.send({ kind: "call", id, name, payload });
113
+ return promise;
114
+ }
115
+
116
+ fire<TIn = Record<string, unknown>>(name: string, payload: TIn): void {
117
+ void this.send({ kind: "fire", name, payload });
118
+ }
119
+
120
+ on<TData = unknown>(name: string, callback: (payload: TData) => void): () => void {
121
+ const existing = this.listeners.get(name) ?? new Set<MessageCallback>();
122
+ existing.add(callback as MessageCallback);
123
+ this.listeners.set(name, existing);
124
+ return () => {
125
+ const current = this.listeners.get(name);
126
+ if (!current) return;
127
+ current.delete(callback as MessageCallback);
128
+ if (current.size === 0) {
129
+ this.listeners.delete(name);
130
+ }
131
+ };
132
+ }
133
+ }
134
+
135
+ const _client = new ConnectionClient();
136
+
137
+ /**
138
+ * connect - Custom channel API
139
+ *
140
+ * Use for app-specific communication:
141
+ * connect.emit('myEvent', { data: 123 });
142
+ * connect.on('myEvent', (data) => { ... });
143
+ */
144
+ export const connect = {
145
+ emit: <TIn = Record<string, unknown>>(name: string, payload: TIn): void => {
146
+ _client.fire(name, payload);
147
+ },
148
+ on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
149
+ return _client.on<TData>(name, callback);
150
+ },
151
+ feature: createFeatureConnect,
152
+ };
153
+
154
+ export type FeatureConnect = {
155
+ on: <TData = unknown>(name: string, callback: (payload: TData) => void) => (() => void);
156
+ emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => void;
157
+ };
158
+
159
+ function scopeName(scope: string, name: string): string {
160
+ if (name.startsWith(`${scope}.`)) return name;
161
+ return `${scope}.${name}`;
162
+ }
163
+
164
+ export function createFeatureConnect(scope: string): FeatureConnect {
165
+ return {
166
+ emit: <TIn = Record<string, unknown>>(name: string, payload: TIn) => {
167
+ const scoped = scopeName(scope, name);
168
+ _client.fire(scoped, payload);
169
+ if (typeof window !== 'undefined') {
170
+ window.dispatchEvent(new CustomEvent(`plusui:${scoped}`, { detail: payload }));
171
+ }
172
+ },
173
+ on: <TData = unknown>(name: string, callback: (payload: TData) => void): (() => void) => {
174
+ const scoped = scopeName(scope, name);
175
+ const off = _client.on<TData>(scoped, callback);
176
+ if (typeof window === 'undefined') return off;
177
+ const domHandler = (event: Event) => {
178
+ callback((event as CustomEvent<TData>).detail);
179
+ };
180
+ window.addEventListener(`plusui:${scoped}`, domHandler as EventListener);
181
+ return () => {
182
+ off();
183
+ window.removeEventListener(`plusui:${scoped}`, domHandler as EventListener);
184
+ };
185
+ },
186
+ };
187
+ }
188
+
189
+ export { _client };
190
+ export default connect;