core-3nweb-client-lib 0.28.4 → 0.29.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.
@@ -1,4 +1,4 @@
1
- import { MsgKeyInfo, MsgKeyRole } from '../keyring';
1
+ import { MsgKeyInfo, MsgKeyRole } from '../../keyring';
2
2
  declare type WritableFS = web3n.files.WritableFS;
3
3
  declare type MsgInfo = web3n.asmail.MsgInfo;
4
4
  /**
@@ -14,9 +14,8 @@ declare type MsgInfo = web3n.asmail.MsgInfo;
14
14
  *
15
15
  */
16
16
  export declare class MsgIndex {
17
- private readonly files;
18
- private readonly records;
19
- private readonly changes;
17
+ private readonly logs;
18
+ private readonly indexed;
20
19
  private constructor();
21
20
  static make(syncedFS: WritableFS): Promise<MsgIndex>;
22
21
  stopSyncing(): void;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (C) 2022 - 2023 3NSoft Inc.
4
+
5
+ This program is free software: you can redistribute it and/or modify it under
6
+ the terms of the GNU General Public License as published by the Free Software
7
+ Foundation, either version 3 of the License, or (at your option) any later
8
+ version.
9
+
10
+ This program is distributed in the hope that it will be useful, but
11
+ WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
+ See the GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along with
16
+ this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.MsgIndex = void 0;
20
+ const logs_n_entries_1 = require("./logs-n-entries");
21
+ const LOGS_DIR = 'logs';
22
+ const INDEX_DIR = 'index';
23
+ async function getOrMakeDirStructure(syncedFS) {
24
+ const logsFS = await syncedFS.writableSubRoot(LOGS_DIR);
25
+ const indexFS = await syncedFS.writableSubRoot(INDEX_DIR);
26
+ return { logsFS, indexFS };
27
+ }
28
+ async function syncDirStructureIfNeeded(syncedFS) {
29
+ // XXX need uploading of initial folders, and of syncedFS itself
30
+ // const logsDirInfo = await logsFS.v!.sync!.status(LOGS_DIR);
31
+ // logsDirInfo.state
32
+ // Or, is this enough?
33
+ // await getRemoteFolderChanges(syncedFS);
34
+ // await uploadFolderChangesIfAny(syncedFS);
35
+ }
36
+ /**
37
+ * This message index stores info for messages present on the server, in the
38
+ * inbox. Records contain message key info, time of delivery, and time of
39
+ * desired removal.
40
+ *
41
+ * Message info with keys is stored in SQLite dbs sharded/partitioned by
42
+ * delivery timestamp. The latest shard, shard without upper time limit
43
+ * is stored in local storage, while all other shards with limits are stored in
44
+ * synced storage. Information in synced storage is a sum of all limited shards
45
+ * and action logs. Action logs
46
+ *
47
+ */
48
+ class MsgIndex {
49
+ constructor(logs, indexed) {
50
+ this.logs = logs;
51
+ this.indexed = indexed;
52
+ Object.seal(this);
53
+ }
54
+ static async make(syncedFS) {
55
+ const { logsFS, indexFS } = await getOrMakeDirStructure(syncedFS);
56
+ await syncDirStructureIfNeeded(syncedFS);
57
+ const logs = await logs_n_entries_1.MsgLogs.makeAndStartSyncing(logsFS);
58
+ let indexed;
59
+ if (global.WebAssembly) {
60
+ indexed = await require('./sql-indexing').makeSqliteBasedIndexedRecords(indexFS);
61
+ }
62
+ else {
63
+ indexed = (0, logs_n_entries_1.makeJsonBasedIndexedRecords)(logs);
64
+ }
65
+ const index = new MsgIndex(logs, indexed);
66
+ return index;
67
+ }
68
+ stopSyncing() {
69
+ this.logs.stopSyncing();
70
+ this.indexed.stopSyncing();
71
+ }
72
+ async add(msgInfo, decrInfo, removeAfter = 0) {
73
+ const msgAlreadyExists = await this.indexed.msgExists(msgInfo);
74
+ if (msgAlreadyExists) {
75
+ return;
76
+ }
77
+ await this.logs.add(msgInfo, decrInfo, removeAfter);
78
+ await this.indexed.add(msgInfo, decrInfo, removeAfter);
79
+ }
80
+ async remove(msgId) {
81
+ const deliveryTS = await this.indexed.remove(msgId);
82
+ if (deliveryTS) {
83
+ await this.logs.remove(msgId, deliveryTS);
84
+ }
85
+ }
86
+ listMsgs(fromTS) {
87
+ return this.indexed.listMsgs(fromTS);
88
+ }
89
+ getKeyFor(msgId, deliveryTS) {
90
+ return this.indexed.getKeyFor(msgId, deliveryTS);
91
+ }
92
+ }
93
+ exports.MsgIndex = MsgIndex;
94
+ Object.freeze(MsgIndex.prototype);
95
+ Object.freeze(MsgIndex);
96
+ Object.freeze(exports);
@@ -0,0 +1,52 @@
1
+ import { MsgKeyInfo, MsgKeyRole } from '../../keyring';
2
+ declare type WritableFS = web3n.files.WritableFS;
3
+ declare type MsgInfo = web3n.asmail.MsgInfo;
4
+ interface MsgOpenedLog {
5
+ msgState: 'opened';
6
+ msgId: string;
7
+ msgType: string;
8
+ deliveryTS: number;
9
+ keyB64: string;
10
+ keyStatus: MsgKeyRole;
11
+ mainObjHeaderOfs: number;
12
+ removeAfter?: number;
13
+ }
14
+ interface MsgRemovedLog {
15
+ msgState: 'removed';
16
+ msgId: string;
17
+ deliveryTS: number;
18
+ }
19
+ declare type MsgLog = MsgOpenedLog | MsgRemovedLog;
20
+ export interface MsgKey {
21
+ msgKey: Uint8Array;
22
+ msgKeyRole: MsgKeyRole;
23
+ mainObjHeaderOfs: number;
24
+ }
25
+ export interface IndexedRecords {
26
+ add(msgInfo: MsgInfo, decrInfo: MsgKeyInfo, removeAfter: number): Promise<void>;
27
+ remove(msgId: string): Promise<number | undefined>;
28
+ listMsgs(fromTS: number | undefined): Promise<MsgInfo[]>;
29
+ getKeyFor(msgId: string, deliveryTS: number): Promise<MsgKey | undefined>;
30
+ msgExists(msgInfo: MsgInfo): Promise<boolean>;
31
+ stopSyncing(): void;
32
+ }
33
+ export declare function makeJsonBasedIndexedRecords(logs: MsgLogs): IndexedRecords;
34
+ export declare class MsgLogs {
35
+ private readonly fs;
36
+ private latest;
37
+ private fileTSs;
38
+ private readonly changeProc;
39
+ private constructor();
40
+ static makeAndStartSyncing(logsFS: WritableFS): Promise<MsgLogs>;
41
+ stopSyncing(): void;
42
+ add(msgInfo: MsgInfo, decrInfo: MsgKeyInfo, removeAfter: number | undefined): Promise<void>;
43
+ private updateLogsFile;
44
+ getLogsFromFile(fileTS: number | undefined): Promise<MsgLog[] | undefined>;
45
+ private fileTSforMsgTS;
46
+ remove(msgId: string, deliveryTS: number): Promise<void>;
47
+ private scheduleRemovalOfMsgOpenedLog;
48
+ getMsgOpenedLog(msgId: string, deliveryTS: number): Promise<MsgOpenedLog | undefined>;
49
+ getLogFileTSs(): number[];
50
+ getLatestMsgLogs(): MsgLog[];
51
+ }
52
+ export {};
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (C) 2016 - 2020, 2023 3NSoft Inc.
4
+
5
+ This program is free software: you can redistribute it and/or modify it under
6
+ the terms of the GNU General Public License as published by the Free Software
7
+ Foundation, either version 3 of the License, or (at your option) any later
8
+ version.
9
+
10
+ This program is distributed in the hope that it will be useful, but
11
+ WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13
+ See the GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along with
16
+ this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.MsgLogs = exports.makeJsonBasedIndexedRecords = void 0;
20
+ const synced_1 = require("../../../../lib-common/processes/synced");
21
+ const buffer_utils_1 = require("../../../../lib-common/buffer-utils");
22
+ const timed_cache_1 = require("../../../../lib-common/timed-cache");
23
+ const error_1 = require("../../../../lib-common/exceptions/error");
24
+ const LIMIT_RECORDS_PER_FILE = 200;
25
+ const LATEST_LOG = 'latest.json';
26
+ const LOG_EXT = '.json';
27
+ const INDEX_FNAME_REGEXP = /^\d+\.json$/;
28
+ function logsFileName(fileTS) {
29
+ return (fileTS ? LATEST_LOG : `${fileTS}${LOG_EXT}`);
30
+ }
31
+ function fileTSOrderComparator(a, b) {
32
+ return (a - b);
33
+ }
34
+ function insertInto(records, rec) {
35
+ if (records.length === 0) {
36
+ records.push(rec);
37
+ return;
38
+ }
39
+ const ts = rec.deliveryTS;
40
+ for (let i = (records.length - 1); i >= 0; i -= 1) {
41
+ if (records[i].deliveryTS <= ts) {
42
+ records.splice(i + 1, 0, rec);
43
+ return;
44
+ }
45
+ }
46
+ records.splice(0, 0, rec);
47
+ }
48
+ function removeMsgOpenedLogFrom(logs, msgId) {
49
+ const indToRm = logs.findIndex(log => ((log.msgId === msgId) && (log.msgState === 'opened')));
50
+ if (indToRm > -1) {
51
+ logs.splice(indToRm, 1);
52
+ return true;
53
+ }
54
+ else {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * This is a catch callback, which returns undefined on file(folder) not found
60
+ * exception, and re-throws all other exceptions/errors.
61
+ */
62
+ function notFoundOrReThrow(exc) {
63
+ if (!exc.notFound) {
64
+ throw exc;
65
+ }
66
+ return;
67
+ }
68
+ function makeJsonBasedIndexedRecords(logs) {
69
+ return new JsonBasedIndexedRecords(logs);
70
+ }
71
+ exports.makeJsonBasedIndexedRecords = makeJsonBasedIndexedRecords;
72
+ function toMsgOpenedLog(msgInfo, decrInfo, removeAfter) {
73
+ if (!decrInfo.key) {
74
+ throw new Error(`Given message decryption info doesn't have a key for message ${msgInfo.msgId}`);
75
+ }
76
+ return {
77
+ msgState: 'opened',
78
+ msgType: msgInfo.msgType,
79
+ msgId: msgInfo.msgId,
80
+ deliveryTS: msgInfo.deliveryTS,
81
+ keyB64: buffer_utils_1.base64.pack(decrInfo.key),
82
+ keyStatus: decrInfo.keyStatus,
83
+ mainObjHeaderOfs: decrInfo.msgKeyPackLen,
84
+ removeAfter
85
+ };
86
+ }
87
+ function keyInfoFrom(log) {
88
+ return {
89
+ msgKey: buffer_utils_1.base64.open(log.keyB64),
90
+ msgKeyRole: log.keyStatus,
91
+ mainObjHeaderOfs: log.mainObjHeaderOfs
92
+ };
93
+ }
94
+ function addOnlyNonRemovedMsgs(dst, ignore, src) {
95
+ for (const log of src) {
96
+ if (log.msgState === 'removed') {
97
+ ignore.add(log.msgId);
98
+ }
99
+ }
100
+ for (const log of src) {
101
+ if ((log.msgState === 'opened') && !ignore.has(log.msgId)) {
102
+ dst.push(log);
103
+ }
104
+ }
105
+ }
106
+ function cutEarlierMsgs(msgs, fromTS) {
107
+ let timedEnd = msgs.length;
108
+ for (let i = (msgs.length - 1); i >= 0; i -= 1) {
109
+ const msg = msgs[i];
110
+ if (msg.deliveryTS < fromTS) {
111
+ timedEnd = i;
112
+ }
113
+ else {
114
+ break;
115
+ }
116
+ }
117
+ if (timedEnd < msgs.length) {
118
+ msgs.splice(timedEnd, (msgs.length - timedEnd));
119
+ }
120
+ }
121
+ class JsonBasedIndexedRecords {
122
+ constructor(logs) {
123
+ this.logs = logs;
124
+ this.cachedLogs = (0, timed_cache_1.makeTimedCache)(10 * 60 * 1000);
125
+ Object.seal(this);
126
+ }
127
+ async add(msgInfo, decrInfo, removeAfter) {
128
+ const msg = toMsgOpenedLog(msgInfo, decrInfo, removeAfter);
129
+ this.cachedLogs.set(msg.msgId, msg);
130
+ }
131
+ async remove(msgId) {
132
+ let foundLog = this.cachedLogs.get(msgId);
133
+ if (foundLog) {
134
+ this.cachedLogs.delete(msgId);
135
+ return foundLog.deliveryTS;
136
+ }
137
+ for (const fileTS of [undefined, ...this.logs.getLogFileTSs()]) {
138
+ let logs;
139
+ if ((fileTS === undefined)) {
140
+ logs = this.logs.getLatestMsgLogs();
141
+ }
142
+ else {
143
+ const logsFromFile = await this.logs.getLogsFromFile(fileTS);
144
+ if (!logsFromFile) {
145
+ continue;
146
+ }
147
+ logs = logsFromFile;
148
+ }
149
+ for (const log of logs) {
150
+ if (log.msgId === msgId) {
151
+ return log.deliveryTS;
152
+ }
153
+ }
154
+ }
155
+ return undefined;
156
+ }
157
+ async listMsgs(fromTS) {
158
+ const ignore = new Set();
159
+ const msgs = [];
160
+ addOnlyNonRemovedMsgs(msgs, ignore, this.logs.getLatestMsgLogs());
161
+ const fileTSs = this.logs.getLogFileTSs();
162
+ if (fromTS === undefined) {
163
+ for (const fileTS of fileTSs) {
164
+ const logs = await this.logs.getLogsFromFile(fileTS);
165
+ if (!logs) {
166
+ continue;
167
+ }
168
+ addOnlyNonRemovedMsgs(msgs, ignore, logs);
169
+ }
170
+ }
171
+ else {
172
+ for (const fileTS of fileTSs) {
173
+ if (fromTS > fileTS) {
174
+ break;
175
+ }
176
+ const logs = await this.logs.getLogsFromFile(fileTS);
177
+ if (!logs) {
178
+ continue;
179
+ }
180
+ addOnlyNonRemovedMsgs(msgs, ignore, logs);
181
+ }
182
+ }
183
+ // note sorting later messages to array's head
184
+ msgs.sort((m1, m2) => (m2.deliveryTS - m1.deliveryTS));
185
+ if (fromTS !== undefined) {
186
+ cutEarlierMsgs(msgs, fromTS);
187
+ }
188
+ for (const log of msgs) {
189
+ this.cachedLogs.set(log.msgId, log);
190
+ }
191
+ return msgs.map(log => ({
192
+ msgId: log.msgId,
193
+ msgType: log.msgType,
194
+ deliveryTS: log.deliveryTS
195
+ }));
196
+ }
197
+ async getKeyFor(msgId, deliveryTS) {
198
+ let log = this.cachedLogs.get(msgId);
199
+ if (log) {
200
+ return keyInfoFrom(log);
201
+ }
202
+ log = await this.logs.getMsgOpenedLog(msgId, deliveryTS);
203
+ if (!log) {
204
+ return undefined;
205
+ }
206
+ this.cachedLogs.set(log.msgId, log);
207
+ return keyInfoFrom(log);
208
+ }
209
+ async msgExists(msgInfo) {
210
+ const foundLog = this.cachedLogs.get(msgInfo.msgId);
211
+ return (!!foundLog && (foundLog.deliveryTS === msgInfo.deliveryTS));
212
+ }
213
+ stopSyncing() { }
214
+ }
215
+ Object.freeze(JsonBasedIndexedRecords.prototype);
216
+ Object.freeze(JsonBasedIndexedRecords);
217
+ class MsgLogs {
218
+ constructor(fs, latest, fileTSs) {
219
+ this.fs = fs;
220
+ this.latest = latest;
221
+ this.fileTSs = fileTSs;
222
+ this.changeProc = new synced_1.SingleProc();
223
+ Object.seal(this);
224
+ }
225
+ static async makeAndStartSyncing(logsFS) {
226
+ const fName = logsFileName(undefined);
227
+ let latest = await logsFS.readJSONFile(fName)
228
+ .catch(notFoundOrReThrow);
229
+ if (!latest) {
230
+ latest = [];
231
+ await logsFS.writeJSONFile(fName, latest, { create: true, exclusive: true });
232
+ }
233
+ const fileTSs = (await logsFS.listFolder('.'))
234
+ .map(f => f.name)
235
+ .filter(fName => fName.match(INDEX_FNAME_REGEXP))
236
+ .map(fName => parseInt(fName.substring(0, fName.length - LOG_EXT.length)))
237
+ .filter(fileTS => !isNaN(fileTS))
238
+ .sort(fileTSOrderComparator);
239
+ const logs = new MsgLogs(logsFS, latest, fileTSs);
240
+ return logs;
241
+ }
242
+ stopSyncing() {
243
+ // XXX fill this with content
244
+ }
245
+ async add(msgInfo, decrInfo, removeAfter) {
246
+ const msg = toMsgOpenedLog(msgInfo, decrInfo, removeAfter);
247
+ return this.changeProc.startOrChain(async () => {
248
+ const fileTS = this.fileTSforMsgTS(msg.deliveryTS);
249
+ const logs = ((fileTS === undefined) ?
250
+ this.latest : await this.getLogsFromFile(fileTS));
251
+ if (!logs) {
252
+ throw (0, error_1.errWithCause)(`${logsFileName(fileTS)} not found`, `Expectation fail: there should be some message records.`);
253
+ }
254
+ insertInto(logs, msg);
255
+ await this.updateLogsFile(fileTS, logs);
256
+ });
257
+ }
258
+ async updateLogsFile(fileTS, logs) {
259
+ await this.fs.writeJSONFile(logsFileName(fileTS), logs, { create: false });
260
+ }
261
+ async getLogsFromFile(fileTS) {
262
+ const fName = logsFileName(undefined);
263
+ const logs = await this.fs.readJSONFile(fName)
264
+ .catch(notFoundOrReThrow);
265
+ return logs;
266
+ }
267
+ fileTSforMsgTS(deliveryTS) {
268
+ if ((this.fileTSs.length === 0)
269
+ || (deliveryTS >= this.fileTSs[this.fileTSs.length - 1])) {
270
+ return undefined;
271
+ }
272
+ let fileTS = this.fileTSs[this.fileTSs.length - 1];
273
+ for (let i = (this.fileTSs.length - 2); i <= 0; i -= 1) {
274
+ if (deliveryTS >= this.fileTSs[i]) {
275
+ break;
276
+ }
277
+ else {
278
+ fileTS = this.fileTSs[i];
279
+ }
280
+ }
281
+ return fileTS;
282
+ }
283
+ async remove(msgId, deliveryTS) {
284
+ const msgRmLog = {
285
+ msgState: 'removed',
286
+ msgId,
287
+ deliveryTS
288
+ };
289
+ return this.changeProc.startOrChain(async () => {
290
+ this.latest.push(msgRmLog);
291
+ const fileTS = this.fileTSforMsgTS(deliveryTS);
292
+ if (fileTS === undefined) {
293
+ removeMsgOpenedLogFrom(this.latest, msgId);
294
+ }
295
+ await this.updateLogsFile(undefined, this.latest);
296
+ if (fileTS !== undefined) {
297
+ this.scheduleRemovalOfMsgOpenedLog(fileTS, msgId);
298
+ }
299
+ });
300
+ }
301
+ scheduleRemovalOfMsgOpenedLog(fileTS, msgId) {
302
+ this.changeProc.startOrChain(async () => {
303
+ const logs = await this.getLogsFromFile(fileTS);
304
+ if (!logs) {
305
+ return;
306
+ }
307
+ const changed = removeMsgOpenedLogFrom(logs, msgId);
308
+ if (!changed) {
309
+ return;
310
+ }
311
+ await this.updateLogsFile(fileTS, logs);
312
+ });
313
+ }
314
+ async getMsgOpenedLog(msgId, deliveryTS) {
315
+ const fileTS = this.fileTSforMsgTS(deliveryTS);
316
+ const logs = ((fileTS === undefined) ?
317
+ this.latest : await this.getLogsFromFile(fileTS));
318
+ if (!logs) {
319
+ return undefined;
320
+ }
321
+ return logs.find(log => ((log.msgId === msgId) && (log.msgState === 'opened')));
322
+ }
323
+ getLogFileTSs() {
324
+ return this.fileTSs.concat([]);
325
+ }
326
+ getLatestMsgLogs() {
327
+ return this.latest.concat([]);
328
+ }
329
+ }
330
+ exports.MsgLogs = MsgLogs;
331
+ Object.freeze(MsgLogs.prototype);
332
+ Object.freeze(MsgLogs);
333
+ Object.freeze(exports);
@@ -0,0 +1,4 @@
1
+ import { IndexedRecords } from './logs-n-entries';
2
+ declare type WritableFS = web3n.files.WritableFS;
3
+ export declare function makeSqliteBasedIndexedRecords(syncedFS: WritableFS): Promise<IndexedRecords>;
4
+ export {};