@inixiative/archive 0.6.0 → 0.7.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.
- package/README.md +32 -42
- package/compose.yaml +24 -3
- package/package.json +10 -5
- package/prisma/migrations/20261001022030_init/migration.sql +119 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +125 -0
- package/src/agents.ts +5 -15
- package/src/cli.ts +187 -146
- package/src/client.ts +15 -15
- package/src/collector.ts +11 -11
- package/src/remote.ts +166 -0
- package/src/schemas.ts +40 -0
- package/src/server.ts +56 -63
- package/src/store.ts +438 -0
- package/src/local.ts +0 -355
package/src/remote.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { defaultHome } from './agents';
|
|
4
|
+
import type { ArchiveChunk, ArchiveSnapshot } from './index';
|
|
5
|
+
import { archiveKey, archiveSnapshotSchema } from './index';
|
|
6
|
+
import type { ArchiveFilter, ArchiveSettings, TagDefinition } from './schemas';
|
|
7
|
+
import type { ArchiveReferenceCount, TagSuggestion } from './tags';
|
|
8
|
+
|
|
9
|
+
export type ArchiveListing = Omit<ArchiveSnapshot, 'entries'> & {
|
|
10
|
+
id: string;
|
|
11
|
+
revision: number;
|
|
12
|
+
digest: string;
|
|
13
|
+
entries: number;
|
|
14
|
+
models: { model: string; effort?: string; entries: number }[];
|
|
15
|
+
suggestedTags: TagSuggestion[];
|
|
16
|
+
references: ArchiveReferenceCount[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class ArchiveRequestError extends Error {
|
|
20
|
+
constructor(
|
|
21
|
+
readonly status: number,
|
|
22
|
+
message: string,
|
|
23
|
+
) {
|
|
24
|
+
super(message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An Archive server over HTTP. Collectors, the CLI and Foundry write through this; only the
|
|
30
|
+
* server holds the database.
|
|
31
|
+
*/
|
|
32
|
+
export class ArchiveClient {
|
|
33
|
+
private cachedSourceId?: string;
|
|
34
|
+
constructor(private readonly options: { url: string; token: string; fetch?: typeof fetch }) {}
|
|
35
|
+
async request<T>(action: string, body: unknown = {}): Promise<T> {
|
|
36
|
+
const response = await (this.options.fetch ?? fetch)(
|
|
37
|
+
new URL(`api/v1/archive/${action}`, this.options.url),
|
|
38
|
+
{
|
|
39
|
+
method: 'POST',
|
|
40
|
+
redirect: 'error',
|
|
41
|
+
signal: AbortSignal.timeout(30_000),
|
|
42
|
+
headers: {
|
|
43
|
+
authorization: `Bearer ${this.options.token}`,
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
const payload = (await response.json().catch(() => ({}))) as { data?: T; error?: string };
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
throw new ArchiveRequestError(response.status, payload.error ?? `Archive ${action} failed`);
|
|
52
|
+
return payload.data as T;
|
|
53
|
+
}
|
|
54
|
+
async sourceId() {
|
|
55
|
+
this.cachedSourceId ??= (await this.request<{ sourceId: string }>('info')).sourceId;
|
|
56
|
+
return this.cachedSourceId;
|
|
57
|
+
}
|
|
58
|
+
head(id: string) {
|
|
59
|
+
return this.request<{ id: string; digest: string; revision: number; projectId: string | null }>(
|
|
60
|
+
'head',
|
|
61
|
+
{ archiveId: id },
|
|
62
|
+
).catch((error) => {
|
|
63
|
+
if (error instanceof ArchiveRequestError && error.status === 404) return undefined;
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Captures a snapshot as the next revision, retrying when another writer got there first. */
|
|
68
|
+
async capture(input: unknown) {
|
|
69
|
+
const snapshot = archiveSnapshotSchema.parse(input);
|
|
70
|
+
const id = archiveKey(snapshot);
|
|
71
|
+
for (let attempt = 0; ; attempt++) {
|
|
72
|
+
const head = await this.head(id);
|
|
73
|
+
try {
|
|
74
|
+
return await this.request<{
|
|
75
|
+
id: string;
|
|
76
|
+
digest: string;
|
|
77
|
+
revision: number;
|
|
78
|
+
changed: boolean;
|
|
79
|
+
}>('ingest', { snapshot, previousDigest: head?.digest ?? null });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const conflict =
|
|
82
|
+
error instanceof ArchiveRequestError &&
|
|
83
|
+
error.status === 409 &&
|
|
84
|
+
error.message === 'Revision conflict';
|
|
85
|
+
if (!conflict || attempt >= 3) throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
read(id: string, revision?: number) {
|
|
90
|
+
return this.request<{
|
|
91
|
+
id: string;
|
|
92
|
+
revision: number;
|
|
93
|
+
digest: string;
|
|
94
|
+
snapshot: ArchiveSnapshot;
|
|
95
|
+
chunks: ArchiveChunk[];
|
|
96
|
+
}>('read', { archiveId: id, ...(revision ? { revision } : {}) }).catch((error) => {
|
|
97
|
+
if (error instanceof ArchiveRequestError && error.status === 404) return undefined;
|
|
98
|
+
throw error;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Every listing matching the filter, following cursors. */
|
|
102
|
+
async list(filter: ArchiveFilter = {}) {
|
|
103
|
+
const archives: ArchiveListing[] = [];
|
|
104
|
+
let beforeId: string | undefined;
|
|
105
|
+
do {
|
|
106
|
+
const page = await this.request<{ archives: ArchiveListing[]; nextCursor: string | null }>(
|
|
107
|
+
'list',
|
|
108
|
+
{ ...filter, limit: 100, ...(beforeId ? { beforeId } : {}) },
|
|
109
|
+
);
|
|
110
|
+
archives.push(...page.archives);
|
|
111
|
+
beforeId = page.nextCursor ?? undefined;
|
|
112
|
+
} while (beforeId);
|
|
113
|
+
return archives;
|
|
114
|
+
}
|
|
115
|
+
search(
|
|
116
|
+
body: ArchiveFilter & { query?: string; budget?: number; limit?: number; beforeId?: string },
|
|
117
|
+
) {
|
|
118
|
+
return this.request<{
|
|
119
|
+
archives: (Omit<ArchiveListing, 'id' | 'entries'> & {
|
|
120
|
+
archiveId: string;
|
|
121
|
+
chunks: ArchiveChunk[];
|
|
122
|
+
tokenCount: number;
|
|
123
|
+
})[];
|
|
124
|
+
tokenCount: number;
|
|
125
|
+
nextCursor: string | null;
|
|
126
|
+
}>('search', body);
|
|
127
|
+
}
|
|
128
|
+
tag(id: string, change: { add?: string[]; remove?: string[] }) {
|
|
129
|
+
return this.request<{ id: string; tags: string[] }>('tag', { archiveId: id, ...change });
|
|
130
|
+
}
|
|
131
|
+
delete(id: string) {
|
|
132
|
+
return this.request<{ deleted: boolean }>('delete', { archiveId: id });
|
|
133
|
+
}
|
|
134
|
+
settings() {
|
|
135
|
+
return this.request<ArchiveSettings>('settings/read');
|
|
136
|
+
}
|
|
137
|
+
updateSettings(change: Partial<ArchiveSettings>) {
|
|
138
|
+
return this.request<ArchiveSettings>('settings/update', change);
|
|
139
|
+
}
|
|
140
|
+
tagDefinitions(actorId?: string) {
|
|
141
|
+
return this.request<{ tags: (TagDefinition & { archives: number })[] }>(
|
|
142
|
+
'tags/list',
|
|
143
|
+
actorId ? { actorId } : {},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The machine's local Archive server (the `archive` block in @inixiative/config ports). */
|
|
149
|
+
export const DEFAULT_URL = 'http://127.0.0.1:4700';
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A client for this machine's Archive: `ARCHIVE_URL` or the default address, with `ARCHIVE_TOKEN`
|
|
153
|
+
* or the home's `server.token`. Undefined until `archive up` (or `init`) has made a token.
|
|
154
|
+
*/
|
|
155
|
+
export function localArchive(options: { home?: string; url?: string; fetch?: typeof fetch } = {}) {
|
|
156
|
+
const tokenFile = join(options.home ?? defaultHome(), 'server.token');
|
|
157
|
+
const token =
|
|
158
|
+
process.env.ARCHIVE_TOKEN ??
|
|
159
|
+
(existsSync(tokenFile) ? readFileSync(tokenFile, 'utf8').trim() : undefined);
|
|
160
|
+
if (!token) return undefined;
|
|
161
|
+
return new ArchiveClient({
|
|
162
|
+
url: options.url ?? process.env.ARCHIVE_URL ?? DEFAULT_URL,
|
|
163
|
+
token,
|
|
164
|
+
fetch: options.fetch,
|
|
165
|
+
});
|
|
166
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ArchiveSnapshot } from './index';
|
|
3
|
+
import { archiveIntegrationSchema, defaultIntegrations } from './tags';
|
|
4
|
+
|
|
5
|
+
export const archiveSettingsSchema = z.strictObject({
|
|
6
|
+
/** Integrations sessions can reference; Kingdom and Foundry display these, Archive owns them. */
|
|
7
|
+
integrations: z
|
|
8
|
+
.array(archiveIntegrationSchema)
|
|
9
|
+
.max(50)
|
|
10
|
+
.refine(
|
|
11
|
+
(list) => new Set(list.map((i) => i.key)).size === list.length,
|
|
12
|
+
'Duplicate integration keys',
|
|
13
|
+
),
|
|
14
|
+
/** Archives whose latest capture is older than this are deleted here. Deletions never sync. */
|
|
15
|
+
retentionDays: z.number().int().positive().max(36_500).nullable(),
|
|
16
|
+
});
|
|
17
|
+
export type ArchiveSettings = z.infer<typeof archiveSettingsSchema>;
|
|
18
|
+
export const defaultSettings: ArchiveSettings = {
|
|
19
|
+
integrations: defaultIntegrations,
|
|
20
|
+
retentionDays: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const tagSchema = z.string().min(1).max(120);
|
|
24
|
+
/** A conceptual tag this archive offers: archive-wide (no actor) or for one actor. */
|
|
25
|
+
export const tagDefinitionSchema = z.strictObject({
|
|
26
|
+
tag: tagSchema,
|
|
27
|
+
actorId: z.string().min(1).max(256).optional(),
|
|
28
|
+
description: z.string().max(500).optional(),
|
|
29
|
+
});
|
|
30
|
+
export type TagDefinition = z.infer<typeof tagDefinitionSchema>;
|
|
31
|
+
|
|
32
|
+
export interface ArchiveFilter {
|
|
33
|
+
projectId?: string;
|
|
34
|
+
source?: ArchiveSnapshot['source'];
|
|
35
|
+
tag?: string;
|
|
36
|
+
actorId?: string;
|
|
37
|
+
reference?: { integration: string; ref: string };
|
|
38
|
+
model?: string;
|
|
39
|
+
effort?: string;
|
|
40
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
archiveReferenceSchema,
|
|
6
|
-
archiveSnapshotSchema,
|
|
7
|
-
selectChunks,
|
|
8
|
-
snapshotDigest,
|
|
9
|
-
} from './index';
|
|
10
|
-
import { archiveSettingsSchema, LocalArchiveStore, tagDefinitionSchema } from './local';
|
|
3
|
+
import { archiveReferenceSchema, archiveSnapshotSchema, selectChunks } from './index';
|
|
4
|
+
import { ArchiveConflict, ArchiveStore, archiveSettingsSchema, tagDefinitionSchema } from './store';
|
|
11
5
|
|
|
12
6
|
const ingestSchema = z.strictObject({
|
|
13
7
|
snapshot: archiveSnapshotSchema,
|
|
@@ -43,19 +37,8 @@ const tagSchema = z.strictObject({
|
|
|
43
37
|
const tagListSchema = z.strictObject({ actorId: z.string().min(1).max(256).optional() });
|
|
44
38
|
const tagRemoveSchema = tagDefinitionSchema.pick({ tag: true, actorId: true });
|
|
45
39
|
|
|
46
|
-
/** Cursor paging over newest-first archives. */
|
|
47
|
-
function page<T extends { id: string }>(items: T[], limit: number, beforeId?: string) {
|
|
48
|
-
const cursor = beforeId ? items.findIndex((item) => item.id === beforeId) : -1;
|
|
49
|
-
if (beforeId && cursor < 0) return undefined;
|
|
50
|
-
const slice = items.slice(cursor + 1, cursor + 1 + limit);
|
|
51
|
-
return {
|
|
52
|
-
items: slice,
|
|
53
|
-
nextCursor: items.length > cursor + 1 + slice.length ? (slice.at(-1)?.id ?? null) : null,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
40
|
/** One deployment, one ownership boundary. No shared cross-tenant database or admin API. */
|
|
58
|
-
export function createArchiveHandler(store:
|
|
41
|
+
export function createArchiveHandler(store: ArchiveStore, token: string) {
|
|
59
42
|
if (token.length < 32)
|
|
60
43
|
throw new Error('ARCHIVE_SERVER_TOKEN must contain at least 32 characters');
|
|
61
44
|
const expected = Buffer.from(`Bearer ${token}`);
|
|
@@ -86,48 +69,53 @@ export function createArchiveHandler(store: LocalArchiveStore, token: string) {
|
|
|
86
69
|
chunks.push(item.value);
|
|
87
70
|
}
|
|
88
71
|
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
72
|
+
if (path === '/api/v1/archive/info') {
|
|
73
|
+
z.strictObject({}).parse(body);
|
|
74
|
+
return json({ data: { sourceId: await store.sourceId(), protocol: 2 } });
|
|
75
|
+
}
|
|
89
76
|
if (path === '/api/v1/archive/ingest') {
|
|
90
77
|
const { snapshot, previousDigest } = ingestSchema.parse(body);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (old && old.digest !== next && old.snapshot.capturedAt > snapshot.capturedAt)
|
|
97
|
-
return json({ error: 'Stale capture' }, 409);
|
|
98
|
-
return json({ data: store.capture(snapshot) });
|
|
78
|
+
return json({ data: await store.capture(snapshot, { previousDigest }) });
|
|
79
|
+
}
|
|
80
|
+
if (path === '/api/v1/archive/head') {
|
|
81
|
+
const head = await store.head(readSchema.pick({ archiveId: true }).parse(body).archiveId);
|
|
82
|
+
return head ? json({ data: head }) : json({ error: 'Archive unavailable' }, 404);
|
|
99
83
|
}
|
|
100
84
|
if (path === '/api/v1/archive/read') {
|
|
101
85
|
const input = readSchema.parse(body),
|
|
102
|
-
archive = store.read(input.archiveId, input.revision);
|
|
86
|
+
archive = await store.read(input.archiveId, input.revision);
|
|
103
87
|
return archive ? json({ data: archive }) : json({ error: 'Archive unavailable' }, 404);
|
|
104
88
|
}
|
|
105
89
|
if (path === '/api/v1/archive/list') {
|
|
106
90
|
const { limit, beforeId, ...filter } = listSchema.parse(body);
|
|
107
|
-
const result =
|
|
91
|
+
const result = await store.page(filter, limit, beforeId);
|
|
108
92
|
if (!result) return json({ error: 'Invalid cursor' }, 400);
|
|
109
93
|
return json({ data: { archives: result.items, nextCursor: result.nextCursor } });
|
|
110
94
|
}
|
|
111
95
|
if (path === '/api/v1/archive/search') {
|
|
112
96
|
const { query, budget, limit, beforeId, ...filter } = searchSchema.parse(body);
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
(a) =>
|
|
118
|
-
|
|
119
|
-
selectChunks(store.read(a.id)!.chunks, query, budget).matchingChunks > 0 ||
|
|
120
|
-
[a.title, ...a.tags].some((text) => text.toLowerCase().includes(query.toLowerCase())),
|
|
97
|
+
const lower = query.toLowerCase();
|
|
98
|
+
const result = await store.page(filter, limit, beforeId, async (listings) => {
|
|
99
|
+
if (!query) return new Set(listings.map((a) => a.id));
|
|
100
|
+
const hits = await store.matching(
|
|
101
|
+
listings.map((a) => a.id),
|
|
102
|
+
query,
|
|
121
103
|
);
|
|
122
|
-
|
|
104
|
+
for (const a of listings)
|
|
105
|
+
if ([a.title, ...a.tags].some((text) => text.toLowerCase().includes(lower)))
|
|
106
|
+
hits.add(a.id);
|
|
107
|
+
return hits;
|
|
108
|
+
});
|
|
123
109
|
if (!result) return json({ error: 'Invalid cursor' }, 400);
|
|
124
|
-
|
|
110
|
+
let remaining = budget;
|
|
111
|
+
const archives = [];
|
|
112
|
+
for (const a of result.items) {
|
|
125
113
|
const selected =
|
|
126
114
|
remaining >= 16
|
|
127
|
-
? selectChunks(store.read(a.id)!.chunks, query, remaining)
|
|
115
|
+
? selectChunks((await store.read(a.id))!.chunks, query, remaining)
|
|
128
116
|
: { chunks: [], tokenCount: 0 };
|
|
129
117
|
remaining -= selected.tokenCount;
|
|
130
|
-
|
|
118
|
+
archives.push({
|
|
131
119
|
archiveId: a.id,
|
|
132
120
|
revision: a.revision,
|
|
133
121
|
digest: a.digest,
|
|
@@ -140,37 +128,43 @@ export function createArchiveHandler(store: LocalArchiveStore, token: string) {
|
|
|
140
128
|
references: a.references,
|
|
141
129
|
coverage: a.coverage,
|
|
142
130
|
...selected,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
145
133
|
return json({
|
|
146
134
|
data: { archives, tokenCount: budget - remaining, nextCursor: result.nextCursor },
|
|
147
135
|
});
|
|
148
136
|
}
|
|
149
137
|
if (path === '/api/v1/archive/tag') {
|
|
150
138
|
const input = tagSchema.parse(body);
|
|
151
|
-
if (!store.read(input.archiveId))
|
|
152
|
-
|
|
139
|
+
if (!(await store.read(input.archiveId)))
|
|
140
|
+
return json({ error: 'Archive unavailable' }, 404);
|
|
141
|
+
return json({ data: await store.tag(input.archiveId, input) });
|
|
153
142
|
}
|
|
154
143
|
if (path === '/api/v1/archive/delete') {
|
|
155
144
|
const input = readSchema.pick({ archiveId: true }).parse(body);
|
|
156
|
-
return store.delete(input.archiveId)
|
|
145
|
+
return (await store.delete(input.archiveId))
|
|
157
146
|
? json({ data: { archiveId: input.archiveId, deleted: true } })
|
|
158
147
|
: json({ error: 'Archive unavailable' }, 404);
|
|
159
148
|
}
|
|
160
149
|
if (path === '/api/v1/archive/settings/read') {
|
|
161
150
|
z.strictObject({}).parse(body);
|
|
162
|
-
return json({ data: store.settings() });
|
|
151
|
+
return json({ data: await store.settings() });
|
|
163
152
|
}
|
|
164
153
|
if (path === '/api/v1/archive/settings/update')
|
|
165
|
-
return json({
|
|
154
|
+
return json({
|
|
155
|
+
data: await store.updateSettings(archiveSettingsSchema.partial().parse(body)),
|
|
156
|
+
});
|
|
166
157
|
if (path === '/api/v1/archive/tags/list')
|
|
167
|
-
return json({
|
|
158
|
+
return json({
|
|
159
|
+
data: { tags: await store.tagDefinitions(tagListSchema.parse(body).actorId) },
|
|
160
|
+
});
|
|
168
161
|
if (path === '/api/v1/archive/tags/define')
|
|
169
|
-
return json({ data: store.defineTag(tagDefinitionSchema.parse(body)) });
|
|
162
|
+
return json({ data: await store.defineTag(tagDefinitionSchema.parse(body)) });
|
|
170
163
|
if (path === '/api/v1/archive/tags/remove')
|
|
171
|
-
return json({ data: { removed: store.undefineTag(tagRemoveSchema.parse(body)) } });
|
|
164
|
+
return json({ data: { removed: await store.undefineTag(tagRemoveSchema.parse(body)) } });
|
|
172
165
|
return json({ error: 'Not found' }, 404);
|
|
173
166
|
} catch (error) {
|
|
167
|
+
if (error instanceof ArchiveConflict) return json({ error: error.message }, 409);
|
|
174
168
|
if (error instanceof z.ZodError || error instanceof SyntaxError)
|
|
175
169
|
return json({ error: 'Invalid archive request' }, 400);
|
|
176
170
|
return json({ error: 'Archive operation failed' }, 500);
|
|
@@ -178,20 +172,20 @@ export function createArchiveHandler(store: LocalArchiveStore, token: string) {
|
|
|
178
172
|
};
|
|
179
173
|
}
|
|
180
174
|
|
|
181
|
-
export function startArchiveServer(options: {
|
|
182
|
-
|
|
175
|
+
export async function startArchiveServer(options: {
|
|
176
|
+
databaseUrl: string;
|
|
183
177
|
token: string;
|
|
184
178
|
port?: number;
|
|
185
179
|
hostname?: string;
|
|
186
180
|
}) {
|
|
187
|
-
const store = new
|
|
188
|
-
// Retention runs at start and hourly; it only ever deletes from this archive.
|
|
189
|
-
store.prune();
|
|
190
|
-
const retention = setInterval(() => store.prune(), 3_600_000);
|
|
191
|
-
retention.unref();
|
|
181
|
+
const store = new ArchiveStore(options.databaseUrl);
|
|
192
182
|
try {
|
|
183
|
+
// Retention runs at start and hourly; it only ever deletes from this archive.
|
|
184
|
+
await store.prune();
|
|
185
|
+
const retention = setInterval(() => void store.prune().catch(() => {}), 3_600_000);
|
|
186
|
+
retention.unref();
|
|
193
187
|
const server = Bun.serve({
|
|
194
|
-
port: options.port ??
|
|
188
|
+
port: options.port ?? 4700,
|
|
195
189
|
hostname: options.hostname ?? '127.0.0.1',
|
|
196
190
|
maxRequestBodySize: 65_000_000,
|
|
197
191
|
fetch: createArchiveHandler(store, options.token),
|
|
@@ -202,12 +196,11 @@ export function startArchiveServer(options: {
|
|
|
202
196
|
async close() {
|
|
203
197
|
clearInterval(retention);
|
|
204
198
|
await server.stop(true);
|
|
205
|
-
store.close();
|
|
199
|
+
await store.close();
|
|
206
200
|
},
|
|
207
201
|
};
|
|
208
202
|
} catch (error) {
|
|
209
|
-
|
|
210
|
-
store.close();
|
|
203
|
+
await store.close();
|
|
211
204
|
throw error;
|
|
212
205
|
}
|
|
213
206
|
}
|