@v1nt1248/3nclient-lib 0.0.14 → 0.0.15

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/package.json CHANGED
@@ -3,12 +3,13 @@
3
3
  "private": false,
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "author": "v1nt1248",
6
- "version": "0.0.14",
6
+ "version": "0.0.15",
7
7
  "description": "Library for 3NWeb clients",
8
8
  "type": "module",
9
9
  "files": [
10
10
  "dist",
11
- "src/components/"
11
+ "src/components/",
12
+ "src/libs"
12
13
  ],
13
14
  "module": "./dist/ui-3n-lib.js",
14
15
  "types": "./dist/index.d.ts",
@@ -0,0 +1,11 @@
1
+ export * from './sqlite-on-3nstorage'
2
+ export {
3
+ makeObservableMethodCaller,
4
+ makeReqRepMethodCaller,
5
+ makeServiceCaller,
6
+ } from './ipc-service-caller'
7
+ export {
8
+ SingleConnectionIPCWrap,
9
+ MultiConnectionIPCWrap,
10
+ IPCWrap,
11
+ } from './ipc-service'
@@ -0,0 +1,121 @@
1
+ /*
2
+ Copyright (C) 2022 - 2023 3NSoft Inc.
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU General Public License as published by the Free Software
6
+ Foundation, either version 3 of the License, or (at your option) any later
7
+ version.
8
+
9
+ This program is distributed in the hope that it will be useful, but
10
+ WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
+ See the GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along with
15
+ this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ /// <reference path="../@types/platform-defs/w3n.d.ts" />
19
+
20
+ import { deserializeArgs, serializeArgs } from "./serialization-for-ipc/json-n-binary";
21
+
22
+ type RPCConnection = web3n.rpc.client.RPCConnection;
23
+ type PassedDatum = web3n.rpc.PassedDatum;
24
+ type Observer<T> = web3n.Observer<T>;
25
+
26
+
27
+ export interface TransformOpts {
28
+ unpackReply?: ((reply: PassedDatum | undefined) => any) | 'noop';
29
+ packRequest?: ((args: any[]) => PassedDatum | undefined) | 'noop';
30
+ }
31
+
32
+ function replyFromPassedDatum(
33
+ data: PassedDatum|undefined, unpack: TransformOpts['unpackReply']
34
+ ): any {
35
+ if (!data) { return; }
36
+ if (unpack) {
37
+ if (unpack === 'noop') {
38
+ return [data.bytes];
39
+ }
40
+ else {
41
+ return unpack(data);
42
+ }
43
+ }
44
+ else {
45
+ const { bytes, passedByReference } = data;
46
+ return (bytes ? deserializeArgs(bytes, passedByReference)[0] : undefined);
47
+ }
48
+ }
49
+
50
+ function argsToPassedDatum(
51
+ args: any[], pack: TransformOpts['packRequest']
52
+ ): PassedDatum|undefined {
53
+ if (args === undefined) { return; }
54
+ if (pack) {
55
+ if (pack === 'noop') {
56
+ if (!ArrayBuffer.isView(args[0])) {
57
+ throw new Error(`Method returned non-binary, while no serialization is set`);
58
+ }
59
+ return { bytes: args[0] as Uint8Array };
60
+ }
61
+ return pack(args);
62
+ }
63
+ else {
64
+ return serializeArgs(args);
65
+ }
66
+ }
67
+
68
+ export function makeReqRepMethodCaller<F extends Function>(
69
+ connection: RPCConnection, method: string, transforms?: TransformOpts
70
+ ): F {
71
+ return (async (...args: any[]) => {
72
+ const req = argsToPassedDatum(args, transforms?.packRequest);
73
+ const reply = await connection.makeRequestReplyCall(method, req);
74
+ return replyFromPassedDatum(reply, transforms?.unpackReply);
75
+ }) as any as F;
76
+ }
77
+
78
+ export function makeObservableMethodCaller<F extends Function>(
79
+ connection: RPCConnection, method: string, transforms?: TransformOpts
80
+ ): F {
81
+ return ((obs: Observer<any>, ...args: any[]) => {
82
+ const req = argsToPassedDatum(args, transforms?.packRequest);
83
+ const obsWrap: Observer<any> = {
84
+ next: data => {
85
+ if (!obs.next) {
86
+ return;
87
+ }
88
+ const ev = replyFromPassedDatum(data, transforms?.unpackReply);
89
+ obs.next(ev);
90
+ },
91
+ complete: obs.complete,
92
+ error: obs.error
93
+ };
94
+ return connection.startObservableCall(method, req, obsWrap);
95
+ }) as any as F;
96
+ }
97
+
98
+ export function makeServiceCaller<T>(
99
+ connection: RPCConnection,
100
+ reqRepMethods?: (keyof T)[], obsMethods?: (keyof T)[],
101
+ transforms?: { [method in keyof T]: TransformOpts; }
102
+ ): Partial<T> {
103
+ const caller = {} as T;
104
+ if (reqRepMethods) {
105
+ for (const method of reqRepMethods) {
106
+ caller[method] = makeReqRepMethodCaller(
107
+ connection, method as string,
108
+ (transforms ? transforms[method] : undefined)
109
+ ) as any;
110
+ }
111
+ }
112
+ if (obsMethods) {
113
+ for (const method of obsMethods) {
114
+ caller[method] = makeObservableMethodCaller(
115
+ connection, method as string,
116
+ (transforms ? transforms[method] : undefined)
117
+ ) as any;
118
+ }
119
+ }
120
+ return caller;
121
+ }
@@ -0,0 +1,422 @@
1
+ /*
2
+ Copyright (C) 2022 - 2023 3NSoft Inc.
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU General Public License as published by the Free Software
6
+ Foundation, either version 3 of the License, or (at your option) any later
7
+ version.
8
+
9
+ This program is distributed in the hope that it will be useful, but
10
+ WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
+ See the GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along with
15
+ this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+ import { deserializeArgs, serializeArgs } from "./serialization-for-ipc/json-n-binary";
18
+
19
+ declare var w3n: web3n.caps.W3N;
20
+ type IncomingConnection = web3n.rpc.service.IncomingConnection;
21
+ type IncomingMsg = web3n.rpc.service.IncomingMsg;
22
+ type OutgoingMsg = web3n.rpc.service.OutgoingMsg;
23
+ type PassedDatum = web3n.rpc.PassedDatum;
24
+ type Observer<T> = web3n.Observer<T>;
25
+
26
+ export type HandleObservingCall = (
27
+ obs: Observer<any>, ...requestArgs: any[]
28
+ ) => (() => void);
29
+
30
+ export type HandleReqReplyCall = (...requestArgs: any[]) => Promise<any>;
31
+
32
+ export interface TransformOpts {
33
+ unpackRequest?: ((req: PassedDatum|undefined) => any[]) | 'noop';
34
+ packReply?: ((reply: any) => PassedDatum|undefined) | 'noop';
35
+ }
36
+
37
+ function surroundWithReqReplySerialization(
38
+ srv: object|undefined, func: HandleReqReplyCall,
39
+ transforms: TransformOpts|undefined
40
+ ): ReqReplyHandler {
41
+ return async requestData => {
42
+ const args = argsFromPassedDatum(
43
+ requestData, transforms?.unpackRequest
44
+ );
45
+ const result = (args ?
46
+ await func.call(srv, ...args) :
47
+ await func.call(srv)
48
+ );
49
+ return toPassedDatum(result, transforms?.packReply);
50
+ };
51
+ }
52
+
53
+ function argsFromPassedDatum(
54
+ data: PassedDatum|undefined, unpack: TransformOpts['unpackRequest']
55
+ ): (any[])|undefined {
56
+ if (!data) { return; }
57
+ if (unpack) {
58
+ if (unpack === 'noop') {
59
+ return [ data.bytes ];
60
+ } else {
61
+ return unpack(data);
62
+ }
63
+ } else {
64
+ const { bytes, passedByReference } = data;
65
+ return (bytes ? deserializeArgs(bytes, passedByReference) : undefined);
66
+ }
67
+ }
68
+
69
+ function toPassedDatum(
70
+ data: any, pack: TransformOpts['packReply']
71
+ ): PassedDatum|undefined {
72
+ if (data === undefined) { return; }
73
+ if (pack) {
74
+ if (pack === 'noop') {
75
+ if (!ArrayBuffer.isView(data)) { throw new Error(
76
+ `Method returned non-binary, while no serialization is set`
77
+ ); }
78
+ return { bytes: data as Uint8Array };
79
+ }
80
+ return pack(data);
81
+ } else {
82
+ return serializeArgs([ data ]);
83
+ }
84
+ }
85
+
86
+ type ReqReplyHandler = (
87
+ req: PassedDatum|undefined
88
+ ) => Promise<PassedDatum|undefined>;
89
+
90
+ function surroundObsWithSerialization(
91
+ srv: object|undefined, func: HandleObservingCall,
92
+ transforms: TransformOpts|undefined
93
+ ): ObservableHandler {
94
+ return (requestData, obs) => {
95
+ const args = argsFromPassedDatum(
96
+ requestData, transforms?.unpackRequest
97
+ );
98
+ const obsWrap: Observer<any> = {
99
+ next: ev => obs.next!(toPassedDatum(ev, transforms?.packReply)),
100
+ complete: obs.complete,
101
+ error: obs.error
102
+ };
103
+ return (args ?
104
+ func.call(srv, obsWrap, ...args) :
105
+ func.call(srv, obsWrap)
106
+ );
107
+ };
108
+ }
109
+
110
+ type ObservableHandler = (
111
+ req: PassedDatum|undefined, obs: Observer<PassedDatum|undefined>
112
+ ) => (() => void);
113
+
114
+
115
+ class ConnectionState {
116
+
117
+ private readonly calls = new Map<number, { cancel?: () => void; }>();
118
+ private isRunning = true;
119
+
120
+ constructor(
121
+ private readonly disconnect: () => void
122
+ ) {}
123
+
124
+ acceptsMsgs(): boolean {
125
+ return this.isRunning;
126
+ }
127
+
128
+ stop(): void {
129
+ if (!this.isRunning) { return; }
130
+ this.isRunning = false;
131
+ this.disconnect();
132
+ for (const { cancel } of this.calls.values()) {
133
+ if (cancel) {
134
+ cancel();
135
+ }
136
+ }
137
+ this.calls.clear();
138
+ }
139
+
140
+ hasCall(callNum: number): boolean {
141
+ return this.calls.has(callNum);
142
+ }
143
+
144
+ cancelCall(callNum: number): void {
145
+ const call = this.calls.get(callNum);
146
+ if (!call) { return; }
147
+ this.calls.delete(callNum);
148
+ if (call.cancel) {
149
+ call.cancel();
150
+ }
151
+ }
152
+
153
+ completeCall(callNum: number): void {
154
+ this.calls.delete(callNum);
155
+ }
156
+
157
+ registerReqReplyCall(callNum: number): void {
158
+ this.calls.set(callNum, {});
159
+ }
160
+
161
+ registerObservableCall(callNum: number, cancel: () => void): void {
162
+ this.calls.set(callNum, { cancel });
163
+ }
164
+
165
+ }
166
+
167
+
168
+ export abstract class IPCWrap {
169
+
170
+ protected readonly connections =
171
+ new Map<IncomingConnection, ConnectionState>();
172
+ private readonly methods = new Map<string, {
173
+ obs?: ObservableHandler; reqRep?: ReqReplyHandler;
174
+ }>();
175
+
176
+ constructor(
177
+ public readonly srvName: string
178
+ ) {}
179
+
180
+ addReqReplyMethod(
181
+ method: string, srv: object|undefined, func: HandleReqReplyCall,
182
+ transforms?: TransformOpts
183
+ ): void {
184
+ this.methods.set(method, {
185
+ reqRep: surroundWithReqReplySerialization(srv, func, transforms)
186
+ });
187
+ }
188
+
189
+ exposeReqReplyMethods<T extends object>(
190
+ srv: T, methods: (keyof T)[],
191
+ transforms?: { [method in keyof T]: TransformOpts; }
192
+ ): void {
193
+ for (const method of methods) {
194
+ const func = srv[method] as HandleReqReplyCall;
195
+ this.addReqReplyMethod(
196
+ func.name, srv, func, (transforms ? transforms[method] : undefined)
197
+ );
198
+ }
199
+ }
200
+
201
+ addObservableMethod(
202
+ method: string, srv: object|undefined, func: HandleObservingCall,
203
+ transforms?: TransformOpts
204
+ ): void {
205
+ this.methods.set(method as string, {
206
+ obs: surroundObsWithSerialization(srv, func, transforms)
207
+ });
208
+ }
209
+
210
+ exposeObservableMethods<T extends object>(
211
+ srv: T, methods: (keyof T)[],
212
+ transforms?: { [method in keyof T]: TransformOpts; }
213
+ ): void {
214
+ for (const method of methods) {
215
+ const func = srv[method] as HandleObservingCall;
216
+ this.addObservableMethod(
217
+ func.name, srv, func, (transforms ? transforms[method] : undefined)
218
+ );
219
+ }
220
+ }
221
+
222
+ private async onMsg(
223
+ connection: IncomingConnection, connectionState: ConnectionState,
224
+ msg: IncomingMsg
225
+ ): Promise<void> {
226
+ if (!connectionState.acceptsMsgs()) { return; }
227
+ if (msg.msgType === 'start') {
228
+ const { callNum, method, data: requestData } = msg;
229
+ if (connectionState.hasCall(callNum)) { return; }
230
+ const m = this.methods.get(method);
231
+ if (!m) {
232
+ await connection.send({
233
+ callNum, callStatus: 'error', err: `Method ${method} not found`
234
+ });
235
+ return;
236
+ }
237
+ const { obs, reqRep } = m;
238
+ if (reqRep) {
239
+ connectionState.registerReqReplyCall(callNum);
240
+ await this.callReqReplyHandler(
241
+ connection, connectionState, reqRep, callNum, requestData
242
+ );
243
+ } else if (obs) {
244
+ const cancelCall = this.callObsHandler(
245
+ connection, connectionState, obs, callNum, requestData
246
+ );
247
+ connectionState.registerObservableCall(callNum, cancelCall);
248
+ }
249
+ } else if (msg.msgType === 'cancel') {
250
+ const { callNum } = msg;
251
+ connectionState.cancelCall(callNum);
252
+ }
253
+ }
254
+
255
+ private async callReqReplyHandler(
256
+ connection: IncomingConnection, connectionState: ConnectionState,
257
+ reqRep: ReqReplyHandler,
258
+ callNum: number, requestData: PassedDatum|undefined
259
+ ): Promise<void> {
260
+ let reply: OutgoingMsg;
261
+ try {
262
+ const data = await reqRep(requestData);
263
+ reply = {
264
+ callNum, callStatus: 'end', data
265
+ };
266
+ } catch (err) {
267
+ reply = {
268
+ callNum, callStatus: 'error', err
269
+ };
270
+ }
271
+ if (!connectionState.hasCall(callNum)) { return; }
272
+ connectionState.completeCall(callNum);
273
+ await connection.send(reply);
274
+ }
275
+
276
+ private callObsHandler(
277
+ connection: IncomingConnection, connectionState: ConnectionState,
278
+ obs: ObservableHandler,
279
+ callNum: number, requestData: PassedDatum|undefined
280
+ ): () => void {
281
+ return obs(requestData, {
282
+ next: data => connection.send({
283
+ callNum, callStatus: 'interim', data
284
+ }),
285
+ complete: async () => {
286
+ if (!connectionState.hasCall(callNum)) { return; }
287
+ connectionState.completeCall(callNum);
288
+ await connection.send({
289
+ callNum, callStatus: 'end'
290
+ });
291
+ },
292
+ error: async err => {
293
+ if (!connectionState.hasCall(callNum)) { return; }
294
+ connectionState.completeCall(callNum);
295
+ await connection.send({
296
+ callNum, callStatus: 'error', err
297
+ });
298
+ }
299
+ });
300
+ }
301
+
302
+ startIPC(): () => void {
303
+ return w3n.rpc!.exposeService!(this.srvName, {
304
+ next: connection => this.onConnection(connection),
305
+ complete: () => this.onListeningCompletion(),
306
+ error: err => this.onListeningError(err)
307
+ });
308
+ }
309
+
310
+ stopIPC(): void {
311
+ for (const connectionState of this.connections.values()) {
312
+ connectionState.stop();
313
+ }
314
+ this.connections.clear();
315
+ }
316
+
317
+ protected async onConnection(
318
+ connection: IncomingConnection
319
+ ): Promise<void> {
320
+ const disconnect = connection.watch({
321
+ next: msg => this.onMsg(connection, connectionState, msg),
322
+ complete: () => this.onConnectionCompletion(
323
+ connection, connectionState
324
+ ),
325
+ error: err => this.onConnectionError(connection, connectionState, err)
326
+ });
327
+ const connectionState = new ConnectionState(() => {
328
+ this.connections.delete(connection);
329
+ disconnect();
330
+ });
331
+ this.connections.set(connection, connectionState);
332
+ }
333
+
334
+ protected async onConnectionCompletion(
335
+ connection: IncomingConnection, connectionState: ConnectionState
336
+ ): Promise<void> {
337
+ this.connections.delete(connection);
338
+ connectionState.stop();
339
+ }
340
+
341
+ protected async onConnectionError(
342
+ connection: IncomingConnection, connectionState: ConnectionState, err: any
343
+ ): Promise<void> {
344
+ this.connections.delete(connection);
345
+ connectionState.stop();
346
+ }
347
+
348
+ protected abstract onListeningCompletion(): Promise<void>;
349
+
350
+ protected abstract onListeningError(err: any): Promise<void>;
351
+
352
+ }
353
+
354
+
355
+ export class SingleConnectionIPCWrap extends IPCWrap {
356
+
357
+ constructor(srvName: string) {
358
+ super(srvName);
359
+ }
360
+
361
+ protected async onListeningCompletion(): Promise<void> {
362
+ w3n.closeSelf!();
363
+ }
364
+
365
+ protected async onListeningError(err: any): Promise<void> {
366
+ if (w3n.log) {
367
+ await w3n.log(
368
+ 'error', `Error in listening for incoming connections`, err
369
+ );
370
+ }
371
+ w3n.closeSelf!();
372
+ }
373
+
374
+ protected async onConnectionCompletion(
375
+ connection: IncomingConnection, connectionState: ConnectionState
376
+ ): Promise<void> {
377
+ super.onConnectionCompletion(connection, connectionState);
378
+ w3n.closeSelf!();
379
+ }
380
+
381
+ protected async onConnectionError(
382
+ connection: IncomingConnection, connectionState: ConnectionState, err: any
383
+ ): Promise<void> {
384
+ super.onConnectionError(connection, connectionState, err);
385
+ if (w3n.log) {
386
+ await w3n.log(
387
+ 'error', `Error in listening for calls`, err
388
+ );
389
+ }
390
+ w3n.closeSelf!();
391
+ }
392
+ }
393
+
394
+
395
+ export class MultiConnectionIPCWrap extends IPCWrap {
396
+
397
+ constructor(srvName: string) {
398
+ super(srvName);
399
+ }
400
+
401
+ protected async onListeningCompletion(): Promise<void> {}
402
+
403
+ protected async onListeningError(err: any): Promise<void> {
404
+ if (w3n.log) {
405
+ await w3n.log(
406
+ 'error', `Error in listening for incoming connections`, err
407
+ );
408
+ }
409
+ }
410
+
411
+ protected async onConnectionError(
412
+ connection: IncomingConnection, connectionState: ConnectionState, err: any
413
+ ): Promise<void> {
414
+ super.onConnectionError(connection, connectionState, err);
415
+ if (w3n.log) {
416
+ await w3n.log(
417
+ 'error', `Error in listening for calls`, err
418
+ );
419
+ }
420
+ }
421
+
422
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ Copyright (C) 2016 - 2018 3NSoft Inc.
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU General Public License as published by the Free Software
6
+ Foundation, either version 3 of the License, or (at your option) any later
7
+ version.
8
+
9
+ This program is distributed in the hope that it will be useful, but
10
+ WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
+ See the GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along with
15
+ this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+
19
+ /**
20
+ * This function creates a copy of json entity with a caveat that Buffer and
21
+ * byte arrays are shared between original and a copy.
22
+ * @param orig is an object, which copy is created. Buffer and byte arrays
23
+ * passed through like primitives.
24
+ * @param excludeTopFields is an optional list of fields to exclude from copy.
25
+ */
26
+ export function copyJSON<T>(orig: T, excludeTopFields?: string[]): T {
27
+ const origType = typeof orig;
28
+ if (origType !== 'object') {
29
+ return ((origType !== 'function') ? orig : (undefined as any));
30
+ }
31
+ if (orig === null) { return (null as any); }
32
+ if (ArrayBuffer.isView(orig)) { return (orig as any); }
33
+ if (Array.isArray(orig)) {
34
+ const arr: any[] = orig;
35
+ const c: any[] = [];
36
+ for (let i=0; i < arr.length; i+=1) {
37
+ c[i] = copyJSON(arr[i]);
38
+ }
39
+ return (c as any);
40
+ } else {
41
+ const c = ({} as T);
42
+ const fields = Object.keys(orig!);
43
+ if (excludeTopFields) {
44
+ for (const f of fields) {
45
+ if (excludeTopFields.includes(f)) { continue; }
46
+ (c as any)[f] = copyJSON<any>((orig as any)[f]);
47
+ }
48
+ } else {
49
+ for (const f of fields) {
50
+ (c as any)[f] = copyJSON<any>((orig as any)[f]);
51
+ }
52
+ }
53
+ return c;
54
+ }
55
+ }