@push.rocks/containerarchive 0.0.2
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/dist_rust/containerarchive_linux_amd64 +0 -0
- package/dist_rust/containerarchive_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.containerarchive.d.ts +87 -0
- package/dist_ts/classes.containerarchive.js +305 -0
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/interfaces.d.ts +184 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/plugins.d.ts +12 -0
- package/dist_ts/plugins.js +15 -0
- package/license +21 -0
- package/npmextra.json +40 -0
- package/package.json +59 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.containerarchive.ts +419 -0
- package/ts/index.ts +2 -0
- package/ts/interfaces.ts +227 -0
- package/ts/plugins.ts +17 -0
package/license
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lossless GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/npmextra.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@git.zone/cli": {
|
|
3
|
+
"projectType": "npm",
|
|
4
|
+
"module": {
|
|
5
|
+
"githost": "code.foss.global",
|
|
6
|
+
"gitscope": "push.rocks",
|
|
7
|
+
"gitrepo": "containerarchive",
|
|
8
|
+
"description": "content-addressed incremental backup engine with deduplication, encryption, and error correction",
|
|
9
|
+
"npmPackagename": "@push.rocks/containerarchive",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"backup",
|
|
13
|
+
"deduplication",
|
|
14
|
+
"content-addressed",
|
|
15
|
+
"incremental",
|
|
16
|
+
"archive",
|
|
17
|
+
"encryption",
|
|
18
|
+
"chunking",
|
|
19
|
+
"fastcdc",
|
|
20
|
+
"pack-files"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"release": {
|
|
24
|
+
"registries": [
|
|
25
|
+
"https://verdaccio.lossless.digital",
|
|
26
|
+
"https://registry.npmjs.org"
|
|
27
|
+
],
|
|
28
|
+
"accessLevel": "public"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"@git.zone/tsrust": {
|
|
32
|
+
"targets": [
|
|
33
|
+
"linux_amd64",
|
|
34
|
+
"linux_arm64"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"@git.zone/tsdoc": {
|
|
38
|
+
"legal": "\n## License and Legal Information\n\nThis module is part of the @push.rocks ecosystem, maintained by Task Venture Capital GmbH.\n\nLicensed under MIT. See LICENSE file for details.\n\nFor questions or commercial licensing, contact: hello@task.vc\n"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@push.rocks/containerarchive",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "content-addressed incremental backup engine with deduplication, encryption, and error correction",
|
|
6
|
+
"main": "dist_ts/index.js",
|
|
7
|
+
"typings": "dist_ts/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "(tstest test/ --verbose --timeout 60)",
|
|
11
|
+
"build": "(tsrust && tsbuild tsfolders --allowimplicitany)"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://code.foss.global/push.rocks/containerarchive.git"
|
|
16
|
+
},
|
|
17
|
+
"author": "Lossless GmbH",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://code.foss.global/push.rocks/containerarchive/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://code.foss.global/push.rocks/containerarchive",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@push.rocks/lik": "^6.0.0",
|
|
25
|
+
"@push.rocks/smartpromise": "^4.0.0",
|
|
26
|
+
"@push.rocks/smartrust": "^1.3.2",
|
|
27
|
+
"@push.rocks/smartrx": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@git.zone/tsbuild": "^2.0.0",
|
|
31
|
+
"@git.zone/tsrun": "^1.0.0",
|
|
32
|
+
"@git.zone/tstest": "^1.0.0",
|
|
33
|
+
"@git.zone/tsrust": "^1.3.0",
|
|
34
|
+
"@types/node": "^22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"ts/**/*",
|
|
38
|
+
"dist/**/*",
|
|
39
|
+
"dist_*/**/*",
|
|
40
|
+
"dist_ts/**/*",
|
|
41
|
+
"assets/**/*",
|
|
42
|
+
"npmextra.json",
|
|
43
|
+
"readme.md"
|
|
44
|
+
],
|
|
45
|
+
"browserslist": [
|
|
46
|
+
"last 1 chrome versions"
|
|
47
|
+
],
|
|
48
|
+
"keywords": [
|
|
49
|
+
"backup",
|
|
50
|
+
"deduplication",
|
|
51
|
+
"content-addressed",
|
|
52
|
+
"incremental",
|
|
53
|
+
"archive",
|
|
54
|
+
"encryption",
|
|
55
|
+
"chunking",
|
|
56
|
+
"fastcdc",
|
|
57
|
+
"pack-files"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autocreated commitinfo by @push.rocks/commitinfo
|
|
3
|
+
*/
|
|
4
|
+
export const commitinfo = {
|
|
5
|
+
name: '@push.rocks/containerarchive',
|
|
6
|
+
version: '0.0.2',
|
|
7
|
+
description: 'content-addressed incremental backup engine with deduplication, encryption, and error correction'
|
|
8
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { commitinfo } from './00_commitinfo_data.js';
|
|
3
|
+
import type {
|
|
4
|
+
TContainerArchiveCommands,
|
|
5
|
+
IInitOptions,
|
|
6
|
+
IOpenOptions,
|
|
7
|
+
IIngestOptions,
|
|
8
|
+
IIngestItem,
|
|
9
|
+
IIngestItemOptions,
|
|
10
|
+
IRestoreOptions,
|
|
11
|
+
ISnapshot,
|
|
12
|
+
ISnapshotFilter,
|
|
13
|
+
IVerifyOptions,
|
|
14
|
+
IVerifyResult,
|
|
15
|
+
IRetentionPolicy,
|
|
16
|
+
IPruneResult,
|
|
17
|
+
IRepairResult,
|
|
18
|
+
IUnlockOptions,
|
|
19
|
+
IIngestProgress,
|
|
20
|
+
IIngestComplete,
|
|
21
|
+
IVerifyError,
|
|
22
|
+
IRepositoryConfig,
|
|
23
|
+
} from './interfaces.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Content-addressed incremental backup engine.
|
|
27
|
+
*
|
|
28
|
+
* Provides deduplicated, optionally encrypted, gzip-compressed storage
|
|
29
|
+
* for arbitrary data streams with full snapshot history.
|
|
30
|
+
*/
|
|
31
|
+
export class ContainerArchive {
|
|
32
|
+
private bridge: plugins.smartrust.RustBridge<TContainerArchiveCommands>;
|
|
33
|
+
private repoPath: string;
|
|
34
|
+
private spawned = false;
|
|
35
|
+
|
|
36
|
+
// Event subjects
|
|
37
|
+
public ingestProgress = new plugins.smartrx.rxjs.Subject<IIngestProgress>();
|
|
38
|
+
public ingestComplete = new plugins.smartrx.rxjs.Subject<IIngestComplete>();
|
|
39
|
+
public verifyError = new plugins.smartrx.rxjs.Subject<IVerifyError>();
|
|
40
|
+
|
|
41
|
+
private constructor(repoPath: string) {
|
|
42
|
+
this.repoPath = plugins.path.resolve(repoPath);
|
|
43
|
+
|
|
44
|
+
const packageDir = plugins.path.resolve(
|
|
45
|
+
plugins.path.dirname(new URL(import.meta.url).pathname),
|
|
46
|
+
'..',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
this.bridge = new plugins.smartrust.RustBridge<TContainerArchiveCommands>({
|
|
50
|
+
binaryName: 'containerarchive',
|
|
51
|
+
localPaths: [
|
|
52
|
+
plugins.path.join(packageDir, 'dist_rust', 'containerarchive'),
|
|
53
|
+
],
|
|
54
|
+
readyTimeoutMs: 30000,
|
|
55
|
+
requestTimeoutMs: 300000,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Listen for events from the Rust binary
|
|
59
|
+
this.bridge.on('event', (event: { event: string; data: any }) => {
|
|
60
|
+
if (event.event === 'progress') {
|
|
61
|
+
this.ingestProgress.next(event.data);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async ensureSpawned(): Promise<void> {
|
|
67
|
+
if (!this.spawned) {
|
|
68
|
+
await this.bridge.spawn();
|
|
69
|
+
this.spawned = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initialize a new repository at the given path.
|
|
75
|
+
*/
|
|
76
|
+
static async init(repoPath: string, options?: IInitOptions): Promise<ContainerArchive> {
|
|
77
|
+
const instance = new ContainerArchive(repoPath);
|
|
78
|
+
await instance.ensureSpawned();
|
|
79
|
+
|
|
80
|
+
await instance.bridge.sendCommand('init', {
|
|
81
|
+
path: instance.repoPath,
|
|
82
|
+
passphrase: options?.passphrase,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return instance;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Open an existing repository at the given path.
|
|
90
|
+
*/
|
|
91
|
+
static async open(repoPath: string, options?: IOpenOptions): Promise<ContainerArchive> {
|
|
92
|
+
const instance = new ContainerArchive(repoPath);
|
|
93
|
+
await instance.ensureSpawned();
|
|
94
|
+
|
|
95
|
+
await instance.bridge.sendCommand('open', {
|
|
96
|
+
path: instance.repoPath,
|
|
97
|
+
passphrase: options?.passphrase,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return instance;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Ingest a single data stream into the repository.
|
|
105
|
+
*/
|
|
106
|
+
async ingest(
|
|
107
|
+
inputStream: NodeJS.ReadableStream,
|
|
108
|
+
options?: IIngestOptions,
|
|
109
|
+
): Promise<ISnapshot> {
|
|
110
|
+
const socketPath = plugins.path.join(
|
|
111
|
+
plugins.os.tmpdir(),
|
|
112
|
+
`containerarchive-ingest-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Create Unix socket server that Rust will connect to
|
|
116
|
+
const { promise: dataTransferred, server } = await this.createSocketServer(
|
|
117
|
+
socketPath,
|
|
118
|
+
inputStream,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Send ingest command to Rust (Rust connects to our socket)
|
|
123
|
+
const result = await this.bridge.sendCommand('ingest', {
|
|
124
|
+
socketPath,
|
|
125
|
+
tags: options?.tags,
|
|
126
|
+
items: options?.items || [{ name: 'data', type: 'data' }],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Wait for data transfer to complete
|
|
130
|
+
await dataTransferred;
|
|
131
|
+
|
|
132
|
+
const snapshot = result.snapshot;
|
|
133
|
+
this.ingestComplete.next({
|
|
134
|
+
snapshotId: snapshot.id,
|
|
135
|
+
originalSize: snapshot.originalSize,
|
|
136
|
+
storedSize: snapshot.storedSize,
|
|
137
|
+
newChunks: snapshot.newChunks,
|
|
138
|
+
reusedChunks: snapshot.reusedChunks,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return snapshot;
|
|
142
|
+
} finally {
|
|
143
|
+
server.close();
|
|
144
|
+
// Clean up socket file
|
|
145
|
+
try {
|
|
146
|
+
plugins.fs.unlinkSync(socketPath);
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ingest multiple data streams as a single multi-item snapshot.
|
|
153
|
+
* Each item gets its own Unix socket for parallel data transfer.
|
|
154
|
+
*/
|
|
155
|
+
async ingestMulti(
|
|
156
|
+
items: IIngestItem[],
|
|
157
|
+
options?: IIngestOptions,
|
|
158
|
+
): Promise<ISnapshot> {
|
|
159
|
+
if (items.length === 0) {
|
|
160
|
+
throw new Error('At least one item is required');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create one socket per item
|
|
164
|
+
const sockets: Array<{
|
|
165
|
+
socketPath: string;
|
|
166
|
+
promise: Promise<void>;
|
|
167
|
+
server: plugins.net.Server;
|
|
168
|
+
}> = [];
|
|
169
|
+
|
|
170
|
+
const itemOptions: Array<{
|
|
171
|
+
name: string;
|
|
172
|
+
type: string;
|
|
173
|
+
socketPath: string;
|
|
174
|
+
}> = [];
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
for (const item of items) {
|
|
178
|
+
const socketPath = plugins.path.join(
|
|
179
|
+
plugins.os.tmpdir(),
|
|
180
|
+
`containerarchive-ingest-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const { promise, server } = await this.createSocketServer(socketPath, item.stream);
|
|
184
|
+
sockets.push({ socketPath, promise, server });
|
|
185
|
+
itemOptions.push({
|
|
186
|
+
name: item.name,
|
|
187
|
+
type: item.type || 'data',
|
|
188
|
+
socketPath,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Send ingestMulti command to Rust with per-item socket paths
|
|
193
|
+
const result = await this.bridge.sendCommand('ingestMulti', {
|
|
194
|
+
tags: options?.tags,
|
|
195
|
+
items: itemOptions,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Wait for all data transfers
|
|
199
|
+
await Promise.all(sockets.map((s) => s.promise));
|
|
200
|
+
|
|
201
|
+
const snapshot = result.snapshot;
|
|
202
|
+
this.ingestComplete.next({
|
|
203
|
+
snapshotId: snapshot.id,
|
|
204
|
+
originalSize: snapshot.originalSize,
|
|
205
|
+
storedSize: snapshot.storedSize,
|
|
206
|
+
newChunks: snapshot.newChunks,
|
|
207
|
+
reusedChunks: snapshot.reusedChunks,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return snapshot;
|
|
211
|
+
} finally {
|
|
212
|
+
for (const s of sockets) {
|
|
213
|
+
s.server.close();
|
|
214
|
+
try { plugins.fs.unlinkSync(s.socketPath); } catch {}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* List snapshots with optional filtering.
|
|
221
|
+
*/
|
|
222
|
+
async listSnapshots(filter?: ISnapshotFilter): Promise<ISnapshot[]> {
|
|
223
|
+
const result = await this.bridge.sendCommand('listSnapshots', {
|
|
224
|
+
filter,
|
|
225
|
+
});
|
|
226
|
+
return result.snapshots;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get details of a specific snapshot.
|
|
231
|
+
*/
|
|
232
|
+
async getSnapshot(snapshotId: string): Promise<ISnapshot> {
|
|
233
|
+
const result = await this.bridge.sendCommand('getSnapshot', {
|
|
234
|
+
snapshotId,
|
|
235
|
+
});
|
|
236
|
+
return result.snapshot;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Restore a snapshot to a ReadableStream.
|
|
241
|
+
*/
|
|
242
|
+
async restore(
|
|
243
|
+
snapshotId: string,
|
|
244
|
+
options?: IRestoreOptions,
|
|
245
|
+
): Promise<NodeJS.ReadableStream> {
|
|
246
|
+
const socketPath = plugins.path.join(
|
|
247
|
+
plugins.os.tmpdir(),
|
|
248
|
+
`containerarchive-restore-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Create Unix socket server that Rust will connect to and write data
|
|
252
|
+
const { readable, server } = await this.createRestoreSocketServer(socketPath);
|
|
253
|
+
|
|
254
|
+
// Send restore command to Rust (Rust connects and writes data)
|
|
255
|
+
// Don't await — let it run in parallel with reading
|
|
256
|
+
this.bridge.sendCommand('restore', {
|
|
257
|
+
snapshotId,
|
|
258
|
+
socketPath,
|
|
259
|
+
item: options?.item,
|
|
260
|
+
}).catch((err) => {
|
|
261
|
+
readable.destroy(err);
|
|
262
|
+
}).finally(() => {
|
|
263
|
+
server.close();
|
|
264
|
+
try {
|
|
265
|
+
plugins.fs.unlinkSync(socketPath);
|
|
266
|
+
} catch {}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return readable;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Verify repository integrity.
|
|
274
|
+
*/
|
|
275
|
+
async verify(options?: IVerifyOptions): Promise<IVerifyResult> {
|
|
276
|
+
const result = await this.bridge.sendCommand('verify', {
|
|
277
|
+
level: options?.level || 'standard',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
for (const error of result.errors) {
|
|
281
|
+
this.verifyError.next(error);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Repair repository (rebuild index, remove stale locks).
|
|
289
|
+
*/
|
|
290
|
+
async repair(): Promise<IRepairResult> {
|
|
291
|
+
return this.bridge.sendCommand('repair', {});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Prune old snapshots and garbage-collect unreferenced packs.
|
|
296
|
+
*/
|
|
297
|
+
async prune(retention: IRetentionPolicy, dryRun = false): Promise<IPruneResult> {
|
|
298
|
+
return this.bridge.sendCommand('prune', {
|
|
299
|
+
retention,
|
|
300
|
+
dryRun,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Rebuild the global index from pack .idx files.
|
|
306
|
+
*/
|
|
307
|
+
async reindex(): Promise<void> {
|
|
308
|
+
await this.bridge.sendCommand('reindex', {});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Remove locks from the repository.
|
|
313
|
+
*/
|
|
314
|
+
async unlock(options?: IUnlockOptions): Promise<void> {
|
|
315
|
+
await this.bridge.sendCommand('unlock', {
|
|
316
|
+
force: options?.force,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Subscribe to events.
|
|
322
|
+
*/
|
|
323
|
+
on(event: 'ingest:progress', handler: (data: IIngestProgress) => void): plugins.smartrx.rxjs.Subscription;
|
|
324
|
+
on(event: 'ingest:complete', handler: (data: IIngestComplete) => void): plugins.smartrx.rxjs.Subscription;
|
|
325
|
+
on(event: 'verify:error', handler: (data: IVerifyError) => void): plugins.smartrx.rxjs.Subscription;
|
|
326
|
+
on(event: string, handler: (data: any) => void): plugins.smartrx.rxjs.Subscription {
|
|
327
|
+
switch (event) {
|
|
328
|
+
case 'ingest:progress':
|
|
329
|
+
return this.ingestProgress.subscribe(handler);
|
|
330
|
+
case 'ingest:complete':
|
|
331
|
+
return this.ingestComplete.subscribe(handler);
|
|
332
|
+
case 'verify:error':
|
|
333
|
+
return this.verifyError.subscribe(handler);
|
|
334
|
+
default:
|
|
335
|
+
throw new Error(`Unknown event: ${event}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Close the repository and terminate the Rust process.
|
|
341
|
+
*/
|
|
342
|
+
async close(): Promise<void> {
|
|
343
|
+
try {
|
|
344
|
+
await this.bridge.sendCommand('close', {});
|
|
345
|
+
} catch {
|
|
346
|
+
// Ignore errors during close
|
|
347
|
+
}
|
|
348
|
+
this.bridge.kill();
|
|
349
|
+
this.spawned = false;
|
|
350
|
+
|
|
351
|
+
this.ingestProgress.complete();
|
|
352
|
+
this.ingestComplete.complete();
|
|
353
|
+
this.verifyError.complete();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ==================== Private Helpers ====================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Create a Unix socket server that accepts a connection from Rust
|
|
360
|
+
* and pipes the inputStream to it (for ingest).
|
|
361
|
+
*/
|
|
362
|
+
private createSocketServer(
|
|
363
|
+
socketPath: string,
|
|
364
|
+
inputStream: NodeJS.ReadableStream,
|
|
365
|
+
): Promise<{
|
|
366
|
+
promise: Promise<void>;
|
|
367
|
+
server: plugins.net.Server;
|
|
368
|
+
}> {
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
const server = plugins.net.createServer((socket) => {
|
|
371
|
+
// Pipe input data to the Rust process via socket
|
|
372
|
+
const readableStream = inputStream as NodeJS.ReadableStream;
|
|
373
|
+
(readableStream as any).pipe(socket);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
server.on('error', reject);
|
|
377
|
+
|
|
378
|
+
server.listen(socketPath, () => {
|
|
379
|
+
const promise = new Promise<void>((res) => {
|
|
380
|
+
server.on('close', () => res());
|
|
381
|
+
// Also resolve after a connection is handled
|
|
382
|
+
server.once('connection', (socket) => {
|
|
383
|
+
socket.on('end', () => {
|
|
384
|
+
res();
|
|
385
|
+
});
|
|
386
|
+
socket.on('error', () => {
|
|
387
|
+
res();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
resolve({ promise, server });
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create a Unix socket server that accepts a connection from Rust
|
|
398
|
+
* and provides a ReadableStream of the received data (for restore).
|
|
399
|
+
*/
|
|
400
|
+
private createRestoreSocketServer(
|
|
401
|
+
socketPath: string,
|
|
402
|
+
): Promise<{
|
|
403
|
+
readable: plugins.stream.PassThrough;
|
|
404
|
+
server: plugins.net.Server;
|
|
405
|
+
}> {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const passthrough = new plugins.stream.PassThrough();
|
|
408
|
+
const server = plugins.net.createServer((socket) => {
|
|
409
|
+
socket.pipe(passthrough);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
server.on('error', reject);
|
|
413
|
+
|
|
414
|
+
server.listen(socketPath, () => {
|
|
415
|
+
resolve({ readable: passthrough, server });
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
package/ts/index.ts
ADDED