kkrpc 0.2.0 → 0.2.1

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # kkrpc
2
2
 
3
+ > This project is created for building extension system for a Tauri app (https://github.com/kunkunsh/kunkun).
4
+ >
5
+ > It's potential can be used in other types of apps, so I open sourced it as a standalone package.
6
+
3
7
  [![NPM Version](https://img.shields.io/npm/v/kkrpc)](https://www.npmjs.com/package/kkrpc)
4
8
  [![JSR Version](https://jsr.io/badges/@kunkun/kkrpc)](https://jsr.io/@kunkun/kkrpc)
5
9
  ![GitHub last commit](https://img.shields.io/github/last-commit/kunkunsh/kkrpc)
@@ -12,11 +16,12 @@
12
16
  - [Documentation by JSR](https://jsr.io/@kunkun/kkrpc/doc)
13
17
  - [Typedoc Documentation](https://kunkunsh.github.io/kkrpc/)
14
18
 
15
- [Excalidraw Diagrams](https://excalidraw.com/#json=otqFU25B2sSjweA4Sbq9l,7-eY_bzFrGAXLNkOVpQ2Tg)
19
+ [Excalidraw Diagrams](https://excalidraw.com/#json=xp6GbAJVAx3nU-h3PhaxW,oYBNvYmCRsQ2XR3MQo73Ug)
16
20
 
17
- ![](https://imgur.com/vR3Lmv0.png)
18
- ![](https://imgur.com/u728aVv.png)
19
- ![](https://imgur.com/2ycWgVQ.png)
21
+ <img src="https://imgur.com/vR3Lmv0.png" style="max-height: 200px;"/>
22
+ <img src="https://i.imgur.com/zmOHNfu.png" style="max-height: 250px;"/>
23
+ <img src="https://imgur.com/u728aVv.png" style="max-height: 400px;"/>
24
+ <img src="https://i.imgur.com/Gu7jH1v.png" style="max-height: 300px;"/>
20
25
 
21
26
  ## Supported Environments
22
27
 
@@ -225,6 +230,10 @@ console.log(data) // { message: "Hello from background!" }
225
230
 
226
231
  Call functions in bun/node/deno processes from Tauri app with JS/TS.
227
232
 
233
+ It allows you to call any JS/TS code in Deno/Bun/Node processes from Tauri app, just like using Electron.
234
+
235
+ Seamless integration with Tauri's official shell plugin and [unlocked shellx plugin](https://github.com/HuakunShen/tauri-plugin-shellx).
236
+
228
237
  ```ts
229
238
  import { RPCChannel, TauriShellStdio } from "kkrpc/browser"
230
239
  import { Child, Command } from "@tauri-apps/plugin-shell"
@@ -250,6 +259,7 @@ async function spawnCmd(runtime: "deno" | "bun" | "node") {
250
259
  throw new Error(`Invalid runtime: ${runtime}, pick either deno or bun`)
251
260
  }
252
261
 
262
+ // monitor stdout/stderr/close/error for debugging and error handling
253
263
  cmd.stdout.on("data", (data) => {
254
264
  console.log("stdout", data)
255
265
  })
@@ -281,3 +291,7 @@ async function spawnCmd(runtime: "deno" | "bun" | "node") {
281
291
  process?.kill()
282
292
  }
283
293
  ```
294
+
295
+ I provided a sample tauri app in `examples/tauri-demo`.
296
+
297
+ ![Sample Tauri App](https://i.imgur.com/nkDwRHk.png)
@@ -0,0 +1,27 @@
1
+ // src/adapters/deno.ts
2
+ import { Buffer } from "node:buffer";
3
+ var DenoIo = class {
4
+ constructor(readStream) {
5
+ this.readStream = readStream;
6
+ this.reader = this.readStream.getReader();
7
+ }
8
+ reader;
9
+ name = "deno-io";
10
+ async read() {
11
+ const { value, done } = await this.reader.read();
12
+ if (done) {
13
+ return null;
14
+ }
15
+ return Buffer.from(value);
16
+ }
17
+ write(data) {
18
+ const encoder = new TextEncoder();
19
+ const encodedData = encoder.encode(data + "\n");
20
+ Deno.stdout.writeSync(encodedData);
21
+ return Promise.resolve();
22
+ }
23
+ };
24
+
25
+ export {
26
+ DenoIo
27
+ };
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // deno-mod.ts
31
+ var deno_mod_exports = {};
32
+ __export(deno_mod_exports, {
33
+ DenoIo: () => DenoIo,
34
+ RPCChannel: () => RPCChannel
35
+ });
36
+ module.exports = __toCommonJS(deno_mod_exports);
37
+
38
+ // src/adapters/deno.ts
39
+ var import_node_buffer = require("buffer");
40
+ var DenoIo = class {
41
+ constructor(readStream) {
42
+ this.readStream = readStream;
43
+ this.reader = this.readStream.getReader();
44
+ }
45
+ reader;
46
+ name = "deno-io";
47
+ async read() {
48
+ const { value, done } = await this.reader.read();
49
+ if (done) {
50
+ return null;
51
+ }
52
+ return import_node_buffer.Buffer.from(value);
53
+ }
54
+ write(data) {
55
+ const encoder = new TextEncoder();
56
+ const encodedData = encoder.encode(data + "\n");
57
+ Deno.stdout.writeSync(encodedData);
58
+ return Promise.resolve();
59
+ }
60
+ };
61
+
62
+ // src/serialization.ts
63
+ var import_superjson = __toESM(require("superjson"), 1);
64
+ function serializeMessage(message) {
65
+ return import_superjson.default.stringify(message) + "\n";
66
+ }
67
+ function deserializeMessage(message) {
68
+ return new Promise((resolve, reject) => {
69
+ try {
70
+ const parsed = import_superjson.default.parse(message);
71
+ resolve(parsed);
72
+ } catch (error) {
73
+ console.error("failed to parse message", typeof message, message, error);
74
+ reject(error);
75
+ }
76
+ });
77
+ }
78
+
79
+ // src/utils.ts
80
+ function generateUUID() {
81
+ return new Array(4).fill(0).map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)).join("-");
82
+ }
83
+
84
+ // src/channel.ts
85
+ var RPCChannel = class {
86
+ constructor(io, options) {
87
+ this.io = io;
88
+ this.apiImplementation = options?.expose;
89
+ this.listen();
90
+ }
91
+ pendingRequests = {};
92
+ callbacks = {};
93
+ callbackCache = /* @__PURE__ */ new Map();
94
+ count = 0;
95
+ messageStr = "";
96
+ apiImplementation;
97
+ /**
98
+ * Exposes a local API implementation that can be called remotely
99
+ * @param api The local API implementation to expose
100
+ */
101
+ expose(api) {
102
+ this.apiImplementation = api;
103
+ }
104
+ /**
105
+ * Returns the IO interface used by this channel
106
+ * @returns The IO interface instance
107
+ */
108
+ getIO() {
109
+ return this.io;
110
+ }
111
+ /**
112
+ * Listens for incoming messages on the IO interface
113
+ * Handles message buffering and parsing
114
+ * @private
115
+ */
116
+ async listen() {
117
+ while (true) {
118
+ const buffer = await this.io.read();
119
+ if (!buffer) {
120
+ continue;
121
+ }
122
+ const bufferStr = buffer.toString("utf-8");
123
+ if (bufferStr.trim().length === 0) {
124
+ continue;
125
+ }
126
+ this.messageStr += bufferStr;
127
+ const lastChar = this.messageStr[this.messageStr.length - 1];
128
+ const msgsSplit = this.messageStr.split("\n");
129
+ const msgs = lastChar === "\n" ? msgsSplit : msgsSplit.slice(0, -1);
130
+ this.messageStr = lastChar === "\n" ? "" : msgsSplit.at(-1) ?? "";
131
+ for (const msgStr of msgs.map((msg) => msg.trim()).filter(Boolean)) {
132
+ if (msgStr.startsWith("{")) {
133
+ this.handleMessageStr(msgStr);
134
+ } else {
135
+ console.log(`(kkrpc stdout passthrough):`, msgStr);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Handles a single message string by parsing and routing it
142
+ * @param messageStr The message string to handle
143
+ * @private
144
+ */
145
+ async handleMessageStr(messageStr) {
146
+ this.count++;
147
+ return deserializeMessage(messageStr).then((parsedMessage) => {
148
+ if (parsedMessage.type === "response") {
149
+ this.handleResponse(parsedMessage);
150
+ } else if (parsedMessage.type === "request") {
151
+ this.handleRequest(parsedMessage);
152
+ } else if (parsedMessage.type === "callback") {
153
+ this.handleCallback(parsedMessage);
154
+ } else {
155
+ console.error("received unknown message type", parsedMessage, typeof parsedMessage);
156
+ }
157
+ }).catch((err) => {
158
+ console.log(`(kkrpc stdout passthrough):`, messageStr);
159
+ });
160
+ }
161
+ /**
162
+ * Calls a method on the remote API
163
+ * @param method The name of the method to call
164
+ * @param args Arguments to pass to the remote method
165
+ * @returns Promise that resolves with the result of the remote call
166
+ */
167
+ callMethod(method, args) {
168
+ return new Promise((resolve, reject) => {
169
+ const messageId = generateUUID();
170
+ this.pendingRequests[messageId] = { resolve, reject };
171
+ const callbackIds = [];
172
+ const processedArgs = args.map((arg) => {
173
+ if (typeof arg === "function") {
174
+ let callbackId = this.callbackCache.get(arg);
175
+ if (!callbackId) {
176
+ callbackId = generateUUID();
177
+ this.callbacks[callbackId] = arg;
178
+ this.callbackCache.set(arg, callbackId);
179
+ } else {
180
+ }
181
+ callbackIds.push(callbackId);
182
+ return `__callback__${callbackId}`;
183
+ }
184
+ return arg;
185
+ });
186
+ const message = {
187
+ id: messageId,
188
+ method,
189
+ args: processedArgs,
190
+ type: "request",
191
+ callbackIds: callbackIds.length > 0 ? callbackIds : void 0
192
+ };
193
+ this.io.write(serializeMessage(message));
194
+ });
195
+ }
196
+ /**
197
+ * Handles responses received from remote method calls
198
+ * @param response The response message to handle
199
+ * @private
200
+ */
201
+ handleResponse(response) {
202
+ const { id } = response;
203
+ const { result, error } = response.args;
204
+ if (this.pendingRequests[id]) {
205
+ if (error) {
206
+ this.pendingRequests[id].reject(new Error(error));
207
+ } else {
208
+ this.pendingRequests[id].resolve(result);
209
+ }
210
+ delete this.pendingRequests[id];
211
+ }
212
+ }
213
+ /**
214
+ * Handles incoming method call requests from the remote endpoint
215
+ * @param request The request message to handle
216
+ * @private
217
+ */
218
+ handleRequest(request) {
219
+ const { id, method, args } = request;
220
+ const methodPath = method.split(".");
221
+ if (!this.apiImplementation) return;
222
+ let target = this.apiImplementation;
223
+ for (let i = 0; i < methodPath.length - 1; i++) {
224
+ target = target[methodPath[i]];
225
+ if (!target) {
226
+ this.sendError(id, `Method path ${method} not found at ${methodPath[i]}`);
227
+ return;
228
+ }
229
+ }
230
+ const finalMethod = methodPath[methodPath.length - 1];
231
+ const targetMethod = target[finalMethod];
232
+ if (typeof targetMethod !== "function") {
233
+ this.sendError(id, `Method ${method} is not a function`);
234
+ return;
235
+ }
236
+ const processedArgs = args.map((arg) => {
237
+ if (typeof arg === "string" && arg.startsWith("__callback__")) {
238
+ const callbackId = arg.slice(12);
239
+ return (...callbackArgs) => {
240
+ this.invokeCallback(callbackId, callbackArgs);
241
+ };
242
+ }
243
+ return arg;
244
+ });
245
+ try {
246
+ const result = targetMethod.apply(target, processedArgs);
247
+ Promise.resolve(result).then((res) => {
248
+ return this.sendResponse(id, res);
249
+ }).catch((err) => this.sendError(id, err.message));
250
+ } catch (error) {
251
+ this.sendError(id, error.message ?? error.toString());
252
+ }
253
+ }
254
+ /**
255
+ * Invokes a callback on the remote endpoint
256
+ * @param callbackId The ID of the callback to invoke
257
+ * @param args Arguments to pass to the callback
258
+ * @private
259
+ */
260
+ invokeCallback(callbackId, args) {
261
+ const message = {
262
+ id: generateUUID(),
263
+ method: callbackId,
264
+ args,
265
+ type: "callback"
266
+ };
267
+ this.io.write(serializeMessage(message));
268
+ }
269
+ /**
270
+ * Handles callback invocations received from the remote endpoint
271
+ * @param message The callback message to handle
272
+ * @private
273
+ */
274
+ handleCallback(message) {
275
+ const { method: callbackId, args } = message;
276
+ const callback = this.callbacks[callbackId];
277
+ if (callback) {
278
+ callback(...args);
279
+ } else {
280
+ console.error(`Callback with id ${callbackId} not found`);
281
+ }
282
+ }
283
+ /**
284
+ * Sends a successful response back to the remote endpoint
285
+ * @param id The ID of the request being responded to
286
+ * @param result The result to send back
287
+ * @private
288
+ */
289
+ sendResponse(id, result) {
290
+ const response = {
291
+ id,
292
+ method: "",
293
+ args: { result },
294
+ type: "response"
295
+ };
296
+ this.io.write(serializeMessage(response));
297
+ }
298
+ /**
299
+ * Sends an error response back to the remote endpoint
300
+ * @param id The ID of the request being responded to
301
+ * @param error The error message to send back
302
+ * @private
303
+ */
304
+ sendError(id, error) {
305
+ const response = {
306
+ id,
307
+ method: "",
308
+ args: { error },
309
+ type: "response"
310
+ };
311
+ this.io.write(serializeMessage(response));
312
+ }
313
+ /**
314
+ * Creates a nested proxy object for chaining remote method calls
315
+ * @param chain Array of method names in the chain
316
+ * @returns Proxy object that transforms property access into remote method calls
317
+ * @private
318
+ */
319
+ createNestedProxy(chain = []) {
320
+ return new Proxy(() => {
321
+ }, {
322
+ get: (_target, prop) => {
323
+ if (typeof prop === "string" && prop !== "then") {
324
+ return this.createNestedProxy([...chain, prop]);
325
+ }
326
+ return void 0;
327
+ },
328
+ apply: (_target, _thisArg, args) => {
329
+ const method = chain.join(".");
330
+ return this.callMethod(method, args);
331
+ }
332
+ });
333
+ }
334
+ /**
335
+ * Returns a proxy object that represents the remote API
336
+ * Methods called on this proxy will be executed on the remote endpoint
337
+ * @returns Proxy object representing the remote API
338
+ */
339
+ getAPI() {
340
+ return this.createNestedProxy();
341
+ }
342
+ /**
343
+ * Frees up memory by clearing stored callbacks and callback cache
344
+ * Useful when dealing with many anonymous callback functions to prevent memory leaks
345
+ */
346
+ freeCallbacks() {
347
+ this.callbacks = {};
348
+ this.callbackCache.clear();
349
+ }
350
+ };
351
+ // Annotate the CommonJS export names for ESM import in node:
352
+ 0 && (module.exports = {
353
+ DenoIo,
354
+ RPCChannel
355
+ });
@@ -0,0 +1,19 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { I as IoInterface } from './channel-D6ZClufP.cjs';
3
+ export { R as RPCChannel } from './channel-D6ZClufP.cjs';
4
+
5
+ /**
6
+ * Stdio implementation for Deno
7
+ * Deno doesn't have `process` object, and have a completely different stdio API,
8
+ * This implementation wrap Deno's `Deno.stdin` and `Deno.stdout` to follow StdioInterface
9
+ */
10
+ declare class DenoIo implements IoInterface {
11
+ private readStream;
12
+ private reader;
13
+ name: string;
14
+ constructor(readStream: ReadableStream<Uint8Array>);
15
+ read(): Promise<Buffer | null>;
16
+ write(data: string): Promise<void>;
17
+ }
18
+
19
+ export { DenoIo };
@@ -0,0 +1,19 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { I as IoInterface } from './channel-D6ZClufP.js';
3
+ export { R as RPCChannel } from './channel-D6ZClufP.js';
4
+
5
+ /**
6
+ * Stdio implementation for Deno
7
+ * Deno doesn't have `process` object, and have a completely different stdio API,
8
+ * This implementation wrap Deno's `Deno.stdin` and `Deno.stdout` to follow StdioInterface
9
+ */
10
+ declare class DenoIo implements IoInterface {
11
+ private readStream;
12
+ private reader;
13
+ name: string;
14
+ constructor(readStream: ReadableStream<Uint8Array>);
15
+ read(): Promise<Buffer | null>;
16
+ write(data: string): Promise<void>;
17
+ }
18
+
19
+ export { DenoIo };
@@ -0,0 +1,10 @@
1
+ import {
2
+ DenoIo
3
+ } from "./chunk-GRCUBSPR.js";
4
+ import {
5
+ RPCChannel
6
+ } from "./chunk-ZSSFWNSX.js";
7
+ export {
8
+ DenoIo,
9
+ RPCChannel
10
+ };
package/dist/mod.d.cts CHANGED
@@ -5,6 +5,7 @@ import { I as IoInterface, D as DestroyableIoInterface } from './channel-D6ZCluf
5
5
  export { R as RPCChannel } from './channel-D6ZClufP.cjs';
6
6
  import { Readable, Writable } from 'node:stream';
7
7
  export { H as HTTPClientIO, a as HTTPServerIO } from './http-CvGfNM3D.cjs';
8
+ export { DenoIo } from './deno-mod.cjs';
8
9
  import '@tauri-apps/plugin-shell';
9
10
 
10
11
  /**
@@ -80,18 +81,4 @@ declare class WebSocketServerIO implements DestroyableIoInterface {
80
81
  signalDestroy(): void;
81
82
  }
82
83
 
83
- /**
84
- * Stdio implementation for Deno
85
- * Deno doesn't have `process` object, and have a completely different stdio API,
86
- * This implementation wrap Deno's `Deno.stdin` and `Deno.stdout` to follow StdioInterface
87
- */
88
- declare class DenoIo implements IoInterface {
89
- private readStream;
90
- private reader;
91
- name: string;
92
- constructor(readStream: ReadableStream<Uint8Array>);
93
- read(): Promise<Buffer | null>;
94
- write(data: string): Promise<void>;
95
- }
96
-
97
- export { BunIo, DenoIo, DestroyableIoInterface, IoInterface, NodeIo, WebSocketClientIO, WebSocketServerIO };
84
+ export { BunIo, DestroyableIoInterface, IoInterface, NodeIo, WebSocketClientIO, WebSocketServerIO };
package/dist/mod.d.ts CHANGED
@@ -5,6 +5,7 @@ import { I as IoInterface, D as DestroyableIoInterface } from './channel-D6ZCluf
5
5
  export { R as RPCChannel } from './channel-D6ZClufP.js';
6
6
  import { Readable, Writable } from 'node:stream';
7
7
  export { H as HTTPClientIO, a as HTTPServerIO } from './http-Bz7mwStC.js';
8
+ export { DenoIo } from './deno-mod.js';
8
9
  import '@tauri-apps/plugin-shell';
9
10
 
10
11
  /**
@@ -80,18 +81,4 @@ declare class WebSocketServerIO implements DestroyableIoInterface {
80
81
  signalDestroy(): void;
81
82
  }
82
83
 
83
- /**
84
- * Stdio implementation for Deno
85
- * Deno doesn't have `process` object, and have a completely different stdio API,
86
- * This implementation wrap Deno's `Deno.stdin` and `Deno.stdout` to follow StdioInterface
87
- */
88
- declare class DenoIo implements IoInterface {
89
- private readStream;
90
- private reader;
91
- name: string;
92
- constructor(readStream: ReadableStream<Uint8Array>);
93
- read(): Promise<Buffer | null>;
94
- write(data: string): Promise<void>;
95
- }
96
-
97
- export { BunIo, DenoIo, DestroyableIoInterface, IoInterface, NodeIo, WebSocketClientIO, WebSocketServerIO };
84
+ export { BunIo, DestroyableIoInterface, IoInterface, NodeIo, WebSocketClientIO, WebSocketServerIO };
package/dist/mod.js CHANGED
@@ -7,6 +7,9 @@ import {
7
7
  ChromeBackgroundIO,
8
8
  ChromeContentIO
9
9
  } from "./chunk-INKNKSKA.js";
10
+ import {
11
+ DenoIo
12
+ } from "./chunk-GRCUBSPR.js";
10
13
  import {
11
14
  HTTPClientIO,
12
15
  HTTPServerIO
@@ -188,30 +191,6 @@ var WebSocketServerIO = class {
188
191
  this.write(DESTROY_SIGNAL);
189
192
  }
190
193
  };
191
-
192
- // src/adapters/deno.ts
193
- import { Buffer as Buffer2 } from "node:buffer";
194
- var DenoIo = class {
195
- constructor(readStream) {
196
- this.readStream = readStream;
197
- this.reader = this.readStream.getReader();
198
- }
199
- reader;
200
- name = "deno-io";
201
- async read() {
202
- const { value, done } = await this.reader.read();
203
- if (done) {
204
- return null;
205
- }
206
- return Buffer2.from(value);
207
- }
208
- write(data) {
209
- const encoder = new TextEncoder();
210
- const encodedData = encoder.encode(data + "\n");
211
- Deno.stdout.writeSync(encodedData);
212
- return Promise.resolve();
213
- }
214
- };
215
194
  export {
216
195
  BunIo,
217
196
  ChromeBackgroundIO,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kkrpc",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,6 +43,15 @@
43
43
  "import": "./dist/http.js",
44
44
  "require": "./dist/http.cjs"
45
45
  },
46
+ "./deno": {
47
+ "types": {
48
+ "import": "./dist/deno-mod.d.ts",
49
+ "require": "./dist/deno-mod.cjs",
50
+ "default": "./dist/deno-mod.js"
51
+ },
52
+ "import": "./dist/deno-mod.js",
53
+ "require": "./dist/deno-mod.cjs"
54
+ },
46
55
  "./chrome": {
47
56
  "types": {
48
57
  "import": "./dist/chrome.d.ts",