@objectstack/metadata-core 5.0.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/LICENSE +202 -0
- package/README.md +40 -0
- package/dist/chunk-F3VZBGKC.cjs +91 -0
- package/dist/chunk-F3VZBGKC.cjs.map +1 -0
- package/dist/chunk-G3VZZW54.js +91 -0
- package/dist/chunk-G3VZZW54.js.map +1 -0
- package/dist/index.cjs +611 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +248 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.js +611 -0
- package/dist/index.js.map +1 -0
- package/dist/repository-DJhCkVYK.d.cts +307 -0
- package/dist/repository-DJhCkVYK.d.ts +307 -0
- package/dist/testing.cjs +254 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +10 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.js +254 -0
- package/dist/testing.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BranchError,
|
|
3
|
+
ConflictError,
|
|
4
|
+
MetadataError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
SchemaValidationError,
|
|
7
|
+
canonicalize,
|
|
8
|
+
hashSpec
|
|
9
|
+
} from "./chunk-G3VZZW54.js";
|
|
10
|
+
|
|
11
|
+
// src/types.ts
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var MetadataTypeSchema = z.enum([
|
|
14
|
+
"object",
|
|
15
|
+
"view",
|
|
16
|
+
"page",
|
|
17
|
+
"dashboard",
|
|
18
|
+
"app",
|
|
19
|
+
"flow",
|
|
20
|
+
"workflow",
|
|
21
|
+
"agent",
|
|
22
|
+
"tool",
|
|
23
|
+
"skill",
|
|
24
|
+
"report",
|
|
25
|
+
"translation",
|
|
26
|
+
"role",
|
|
27
|
+
"permission",
|
|
28
|
+
"policy",
|
|
29
|
+
"api",
|
|
30
|
+
"endpoint",
|
|
31
|
+
"datasource",
|
|
32
|
+
"cube",
|
|
33
|
+
"settings"
|
|
34
|
+
]).describe("Canonical metadata type name");
|
|
35
|
+
var MetaRefSchema = z.object({
|
|
36
|
+
org: z.string().min(1).describe('Tenant/org identifier; "system" for built-ins'),
|
|
37
|
+
type: MetadataTypeSchema,
|
|
38
|
+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe("Snake_case machine name"),
|
|
39
|
+
version: z.string().optional().describe("Optional version pin (content hash); omit for HEAD")
|
|
40
|
+
});
|
|
41
|
+
function refKey(ref) {
|
|
42
|
+
return `${ref.org}/${ref.type}/${ref.name}`;
|
|
43
|
+
}
|
|
44
|
+
var MetadataItemSchema = z.object({
|
|
45
|
+
ref: MetaRefSchema,
|
|
46
|
+
body: z.record(z.string(), z.unknown()).describe("Canonical Zod-normalised spec"),
|
|
47
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/).describe("sha256(canonicalize(body))"),
|
|
48
|
+
parentHash: z.string().nullable().describe("Hash this version was derived from; null for first version"),
|
|
49
|
+
authoredBy: z.string().describe('Identity of the writer (user id, "cli", "ai:claude", \u2026)'),
|
|
50
|
+
authoredAt: z.string().describe("ISO-8601 timestamp"),
|
|
51
|
+
message: z.string().optional().describe("Optional commit message"),
|
|
52
|
+
seq: z.number().int().nonnegative().describe("Sequence number this write produced in the org log"),
|
|
53
|
+
schemaVersion: z.string().optional().describe("Zod schema version that wrote this spec (M3 codemod hook)")
|
|
54
|
+
});
|
|
55
|
+
var MetadataOpSchema = z.enum(["create", "update", "delete", "rename"]);
|
|
56
|
+
var MetadataEventSchema = z.object({
|
|
57
|
+
seq: z.number().int().nonnegative(),
|
|
58
|
+
op: MetadataOpSchema,
|
|
59
|
+
ref: MetaRefSchema,
|
|
60
|
+
hash: z.string().nullable(),
|
|
61
|
+
parentHash: z.string().nullable(),
|
|
62
|
+
previousName: z.string().optional().describe('Set on op="rename"'),
|
|
63
|
+
actor: z.string(),
|
|
64
|
+
message: z.string().optional(),
|
|
65
|
+
ts: z.string(),
|
|
66
|
+
source: z.string().describe('Origin label: "fs", "studio", "rest", "ai", "git-import", \u2026')
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// src/repository.ts
|
|
70
|
+
var LAYER_SOURCE = /* @__PURE__ */ Symbol.for("objectstack.metadata.layer-source");
|
|
71
|
+
|
|
72
|
+
// src/in-memory-repository.ts
|
|
73
|
+
var orgKey = (ref) => ref.org;
|
|
74
|
+
var matchesFilter = (ref, filter) => {
|
|
75
|
+
if (filter.org && filter.org !== ref.org) return false;
|
|
76
|
+
if (filter.type && filter.type !== ref.type) return false;
|
|
77
|
+
if (filter.name && filter.name !== ref.name) return false;
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
var InMemoryRepository = class {
|
|
81
|
+
constructor(opts = {}) {
|
|
82
|
+
this.items = /* @__PURE__ */ new Map();
|
|
83
|
+
/** Per-org event log. */
|
|
84
|
+
this.logs = /* @__PURE__ */ new Map();
|
|
85
|
+
/** Next seq per org. */
|
|
86
|
+
this.seqs = /* @__PURE__ */ new Map();
|
|
87
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
88
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
89
|
+
}
|
|
90
|
+
async get(ref) {
|
|
91
|
+
const item = this.items.get(refKey(ref));
|
|
92
|
+
if (!item) return null;
|
|
93
|
+
if (ref.version && item.hash !== ref.version) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return clone(item);
|
|
97
|
+
}
|
|
98
|
+
async put(ref, spec, opts) {
|
|
99
|
+
const key = refKey(ref);
|
|
100
|
+
const current = this.items.get(key);
|
|
101
|
+
const currentHead = current?.hash ?? null;
|
|
102
|
+
if ((opts.parentVersion ?? null) !== currentHead) {
|
|
103
|
+
throw new ConflictError(ref, opts.parentVersion ?? null, currentHead);
|
|
104
|
+
}
|
|
105
|
+
const hash = hashSpec(spec);
|
|
106
|
+
if (current && current.hash === hash) {
|
|
107
|
+
return { version: hash, seq: current.seq, item: clone(current) };
|
|
108
|
+
}
|
|
109
|
+
const seq = this.bumpSeq(ref);
|
|
110
|
+
const ts = this.now().toISOString();
|
|
111
|
+
const item = {
|
|
112
|
+
ref: { ...ref, version: void 0 },
|
|
113
|
+
body: clonePlain(spec),
|
|
114
|
+
hash,
|
|
115
|
+
parentHash: currentHead,
|
|
116
|
+
authoredBy: opts.actor,
|
|
117
|
+
authoredAt: ts,
|
|
118
|
+
message: opts.message,
|
|
119
|
+
seq
|
|
120
|
+
};
|
|
121
|
+
this.items.set(key, item);
|
|
122
|
+
const evt = {
|
|
123
|
+
seq,
|
|
124
|
+
op: current ? "update" : "create",
|
|
125
|
+
ref: { ...ref, version: void 0 },
|
|
126
|
+
hash,
|
|
127
|
+
parentHash: currentHead,
|
|
128
|
+
actor: opts.actor,
|
|
129
|
+
message: opts.message,
|
|
130
|
+
ts,
|
|
131
|
+
source: opts.source ?? "in-memory"
|
|
132
|
+
};
|
|
133
|
+
this.appendEvent(ref, evt);
|
|
134
|
+
return { version: hash, seq, item: clone(item) };
|
|
135
|
+
}
|
|
136
|
+
async delete(ref, opts) {
|
|
137
|
+
const key = refKey(ref);
|
|
138
|
+
const current = this.items.get(key);
|
|
139
|
+
const currentHead = current?.hash ?? null;
|
|
140
|
+
if (currentHead !== opts.parentVersion) {
|
|
141
|
+
throw new ConflictError(ref, opts.parentVersion, currentHead);
|
|
142
|
+
}
|
|
143
|
+
this.items.delete(key);
|
|
144
|
+
const seq = this.bumpSeq(ref);
|
|
145
|
+
const ts = this.now().toISOString();
|
|
146
|
+
const evt = {
|
|
147
|
+
seq,
|
|
148
|
+
op: "delete",
|
|
149
|
+
ref: { ...ref, version: void 0 },
|
|
150
|
+
hash: null,
|
|
151
|
+
parentHash: currentHead,
|
|
152
|
+
actor: opts.actor,
|
|
153
|
+
message: opts.message,
|
|
154
|
+
ts,
|
|
155
|
+
source: opts.source ?? "in-memory"
|
|
156
|
+
};
|
|
157
|
+
this.appendEvent(ref, evt);
|
|
158
|
+
return { seq };
|
|
159
|
+
}
|
|
160
|
+
async *list(filter) {
|
|
161
|
+
const limit = filter.limit ?? Infinity;
|
|
162
|
+
let yielded = 0;
|
|
163
|
+
for (const item of this.items.values()) {
|
|
164
|
+
if (!matchesFilter(item.ref, filter)) continue;
|
|
165
|
+
if (filter.nameContains && !item.ref.name.includes(filter.nameContains)) continue;
|
|
166
|
+
const { body, ...header } = item;
|
|
167
|
+
void body;
|
|
168
|
+
yield clone(header);
|
|
169
|
+
if (++yielded >= limit) return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async *history(ref, opts = {}) {
|
|
173
|
+
const log = this.logs.get(orgKey(ref)) ?? [];
|
|
174
|
+
const since = opts.sinceSeq ?? -1;
|
|
175
|
+
const limit = opts.limit ?? Infinity;
|
|
176
|
+
let yielded = 0;
|
|
177
|
+
for (const evt of log) {
|
|
178
|
+
if (evt.seq <= since) continue;
|
|
179
|
+
if (evt.ref.type !== ref.type || evt.ref.name !== ref.name) continue;
|
|
180
|
+
yield clone(evt);
|
|
181
|
+
if (++yielded >= limit) return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
watch(filter, since) {
|
|
185
|
+
const queue = [];
|
|
186
|
+
let waiter = null;
|
|
187
|
+
let closed = false;
|
|
188
|
+
const delivered = /* @__PURE__ */ new Set();
|
|
189
|
+
const evtKey = (e) => `${orgKey(e.ref)}#${e.seq}`;
|
|
190
|
+
const subscriber = {
|
|
191
|
+
filter,
|
|
192
|
+
closed: false,
|
|
193
|
+
push: (evt) => {
|
|
194
|
+
if (subscriber.closed) return;
|
|
195
|
+
if (waiter) {
|
|
196
|
+
const k = evtKey(evt);
|
|
197
|
+
if (delivered.has(k)) return;
|
|
198
|
+
delivered.add(k);
|
|
199
|
+
const w = waiter;
|
|
200
|
+
waiter = null;
|
|
201
|
+
w({ value: clone(evt), done: false });
|
|
202
|
+
} else {
|
|
203
|
+
queue.push(evt);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
this.subscribers.add(subscriber);
|
|
208
|
+
const replay = [];
|
|
209
|
+
for (const ok of this.orgKeysMatching(filter)) {
|
|
210
|
+
const log = this.logs.get(ok) ?? [];
|
|
211
|
+
for (const evt of log) {
|
|
212
|
+
if (typeof since === "number" && evt.seq <= since) continue;
|
|
213
|
+
if (!matchesFilter(evt.ref, filter)) continue;
|
|
214
|
+
replay.push(evt);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
replay.sort((a, b) => a.seq - b.seq);
|
|
218
|
+
let replayIdx = 0;
|
|
219
|
+
const drainQueueOrReplay = () => {
|
|
220
|
+
while (replayIdx < replay.length) {
|
|
221
|
+
const evt = replay[replayIdx++];
|
|
222
|
+
const k = evtKey(evt);
|
|
223
|
+
if (delivered.has(k)) continue;
|
|
224
|
+
delivered.add(k);
|
|
225
|
+
return { value: clone(evt), done: false };
|
|
226
|
+
}
|
|
227
|
+
while (queue.length > 0) {
|
|
228
|
+
const evt = queue.shift();
|
|
229
|
+
const k = evtKey(evt);
|
|
230
|
+
if (delivered.has(k)) continue;
|
|
231
|
+
delivered.add(k);
|
|
232
|
+
return { value: clone(evt), done: false };
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
};
|
|
236
|
+
const close = () => {
|
|
237
|
+
if (!closed) {
|
|
238
|
+
closed = true;
|
|
239
|
+
subscriber.closed = true;
|
|
240
|
+
this.subscribers.delete(subscriber);
|
|
241
|
+
if (waiter) {
|
|
242
|
+
const w = waiter;
|
|
243
|
+
waiter = null;
|
|
244
|
+
w({ value: void 0, done: true });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { value: void 0, done: true };
|
|
248
|
+
};
|
|
249
|
+
const iterator = {
|
|
250
|
+
next: () => {
|
|
251
|
+
if (closed) return Promise.resolve({ value: void 0, done: true });
|
|
252
|
+
const immediate = drainQueueOrReplay();
|
|
253
|
+
if (immediate) return Promise.resolve(immediate);
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
waiter = resolve;
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
return: () => Promise.resolve(close()),
|
|
259
|
+
throw: (err) => {
|
|
260
|
+
close();
|
|
261
|
+
return Promise.reject(err);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
[Symbol.asyncIterator]: () => iterator
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
269
|
+
bumpSeq(ref) {
|
|
270
|
+
const ok = orgKey(ref);
|
|
271
|
+
const next = (this.seqs.get(ok) ?? 0) + 1;
|
|
272
|
+
this.seqs.set(ok, next);
|
|
273
|
+
return next;
|
|
274
|
+
}
|
|
275
|
+
appendEvent(ref, evt) {
|
|
276
|
+
const ok = orgKey(ref);
|
|
277
|
+
const log = this.logs.get(ok) ?? [];
|
|
278
|
+
log.push(evt);
|
|
279
|
+
this.logs.set(ok, log);
|
|
280
|
+
for (const sub of this.subscribers) {
|
|
281
|
+
if (sub.closed) continue;
|
|
282
|
+
if (!matchesFilter(evt.ref, sub.filter)) continue;
|
|
283
|
+
sub.push(evt);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
orgKeysMatching(filter) {
|
|
287
|
+
const keys = [];
|
|
288
|
+
for (const ok of this.logs.keys()) {
|
|
289
|
+
if (filter.org && filter.org !== ok) continue;
|
|
290
|
+
keys.push(ok);
|
|
291
|
+
}
|
|
292
|
+
return keys;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
function clone(value) {
|
|
296
|
+
return JSON.parse(JSON.stringify(value));
|
|
297
|
+
}
|
|
298
|
+
function clonePlain(value) {
|
|
299
|
+
return JSON.parse(JSON.stringify(value));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/cache.ts
|
|
303
|
+
var MetadataCache = class {
|
|
304
|
+
constructor(repo, opts = {}) {
|
|
305
|
+
/** LRU is implemented via insertion-order Map; touch on get/set. */
|
|
306
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
307
|
+
this.bytes = 0;
|
|
308
|
+
/** De-duplicate concurrent fetches for the same key. */
|
|
309
|
+
this.inflight = /* @__PURE__ */ new Map();
|
|
310
|
+
/**
|
|
311
|
+
* Generation counter incremented on every invalidation. Each in-flight
|
|
312
|
+
* fetch captures the gen at start; if the gen changes before it
|
|
313
|
+
* resolves, the result is NOT cached.
|
|
314
|
+
*/
|
|
315
|
+
this.fetchGen = /* @__PURE__ */ new Map();
|
|
316
|
+
this.watchIterator = null;
|
|
317
|
+
this.watchClosed = false;
|
|
318
|
+
this.watchLoop = null;
|
|
319
|
+
this.stats = {
|
|
320
|
+
entries: 0,
|
|
321
|
+
bytes: 0,
|
|
322
|
+
hits: 0,
|
|
323
|
+
misses: 0,
|
|
324
|
+
invalidations: 0,
|
|
325
|
+
coalesced: 0
|
|
326
|
+
};
|
|
327
|
+
this.repo = repo;
|
|
328
|
+
this.maxEntries = opts.maxEntries ?? 1024;
|
|
329
|
+
this.maxBytes = opts.maxBytes ?? 8 * 1024 * 1024;
|
|
330
|
+
this.watchFilter = opts.watchFilter ?? {};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Start the background watch subscription. Idempotent; safe to call
|
|
334
|
+
* multiple times. Caller is responsible for calling `close()` when
|
|
335
|
+
* the cache is no longer needed.
|
|
336
|
+
*/
|
|
337
|
+
start() {
|
|
338
|
+
if (this.watchIterator || this.watchClosed) return;
|
|
339
|
+
const iter = this.repo.watch(this.watchFilter)[Symbol.asyncIterator]();
|
|
340
|
+
this.watchIterator = iter;
|
|
341
|
+
this.watchLoop = (async () => {
|
|
342
|
+
try {
|
|
343
|
+
while (!this.watchClosed) {
|
|
344
|
+
const { value, done } = await iter.next();
|
|
345
|
+
if (done) return;
|
|
346
|
+
this.applyEvent(value);
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
})();
|
|
351
|
+
}
|
|
352
|
+
/** Tear down the watch subscription and clear in-flight tracking. */
|
|
353
|
+
async close() {
|
|
354
|
+
this.watchClosed = true;
|
|
355
|
+
if (this.watchIterator?.return) {
|
|
356
|
+
try {
|
|
357
|
+
await this.watchIterator.return(void 0);
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
this.watchIterator = null;
|
|
362
|
+
if (this.watchLoop) {
|
|
363
|
+
try {
|
|
364
|
+
await this.watchLoop;
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
this.watchLoop = null;
|
|
368
|
+
}
|
|
369
|
+
this.inflight.clear();
|
|
370
|
+
}
|
|
371
|
+
/** Read with cache. Coalesces concurrent reads for the same key. */
|
|
372
|
+
async get(ref) {
|
|
373
|
+
const key = refKey(ref);
|
|
374
|
+
const cached = this.entries.get(key);
|
|
375
|
+
if (cached) {
|
|
376
|
+
this.entries.delete(key);
|
|
377
|
+
this.entries.set(key, cached);
|
|
378
|
+
cached.hits += 1;
|
|
379
|
+
this.stats.hits += 1;
|
|
380
|
+
return cached.item ? clone2(cached.item) : null;
|
|
381
|
+
}
|
|
382
|
+
const existing = this.inflight.get(key);
|
|
383
|
+
if (existing) {
|
|
384
|
+
this.stats.coalesced += 1;
|
|
385
|
+
const item2 = await existing;
|
|
386
|
+
return item2 ? clone2(item2) : null;
|
|
387
|
+
}
|
|
388
|
+
this.stats.misses += 1;
|
|
389
|
+
const gen = this.fetchGen.get(key) ?? 0;
|
|
390
|
+
const promise = this.repo.get(ref).then((item2) => {
|
|
391
|
+
if ((this.fetchGen.get(key) ?? 0) === gen) {
|
|
392
|
+
this.cacheSet(key, item2);
|
|
393
|
+
}
|
|
394
|
+
return item2;
|
|
395
|
+
}).finally(() => {
|
|
396
|
+
this.inflight.delete(key);
|
|
397
|
+
});
|
|
398
|
+
this.inflight.set(key, promise);
|
|
399
|
+
const item = await promise;
|
|
400
|
+
return item ? clone2(item) : null;
|
|
401
|
+
}
|
|
402
|
+
/** Drop a single entry by ref. */
|
|
403
|
+
invalidate(ref) {
|
|
404
|
+
const key = refKey(ref);
|
|
405
|
+
this.bumpGen(key);
|
|
406
|
+
const removed = this.entries.get(key);
|
|
407
|
+
if (removed) {
|
|
408
|
+
this.bytes -= removed.size;
|
|
409
|
+
this.entries.delete(key);
|
|
410
|
+
}
|
|
411
|
+
this.stats.invalidations += 1;
|
|
412
|
+
}
|
|
413
|
+
/** Drop the entire cache (e.g. on a reset). */
|
|
414
|
+
clear() {
|
|
415
|
+
this.entries.clear();
|
|
416
|
+
this.bytes = 0;
|
|
417
|
+
for (const key of this.inflight.keys()) this.bumpGen(key);
|
|
418
|
+
}
|
|
419
|
+
getStats() {
|
|
420
|
+
return {
|
|
421
|
+
...this.stats,
|
|
422
|
+
entries: this.entries.size,
|
|
423
|
+
bytes: this.bytes
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// ── Internals ───────────────────────────────────────────────────────
|
|
427
|
+
applyEvent(evt) {
|
|
428
|
+
const ref = evt.ref;
|
|
429
|
+
this.invalidate(ref);
|
|
430
|
+
if (evt.op === "rename" && evt.previousName) {
|
|
431
|
+
this.invalidate({ ...ref, name: evt.previousName });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
cacheSet(key, item) {
|
|
435
|
+
const size = item ? estimateSize(item) : 0;
|
|
436
|
+
const existing = this.entries.get(key);
|
|
437
|
+
if (existing) this.bytes -= existing.size;
|
|
438
|
+
this.entries.set(key, { item, size, hits: 0 });
|
|
439
|
+
this.bytes += size;
|
|
440
|
+
this.evictIfNeeded();
|
|
441
|
+
}
|
|
442
|
+
evictIfNeeded() {
|
|
443
|
+
while (this.entries.size > this.maxEntries || this.bytes > this.maxBytes) {
|
|
444
|
+
const first = this.entries.keys().next();
|
|
445
|
+
if (first.done) break;
|
|
446
|
+
const key = first.value;
|
|
447
|
+
const entry = this.entries.get(key);
|
|
448
|
+
this.bytes -= entry.size;
|
|
449
|
+
this.entries.delete(key);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
bumpGen(key) {
|
|
453
|
+
this.fetchGen.set(key, (this.fetchGen.get(key) ?? 0) + 1);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
function estimateSize(item) {
|
|
457
|
+
try {
|
|
458
|
+
return JSON.stringify(item.body).length * 2 + 256;
|
|
459
|
+
} catch {
|
|
460
|
+
return 1024;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function clone2(value) {
|
|
464
|
+
return JSON.parse(JSON.stringify(value));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/layered-repository.ts
|
|
468
|
+
var tagSource = (label, evt) => ({
|
|
469
|
+
...evt,
|
|
470
|
+
source: `${label}:${evt.source}`
|
|
471
|
+
});
|
|
472
|
+
var LayeredRepository = class {
|
|
473
|
+
constructor(opts) {
|
|
474
|
+
if (!opts.layers.length) {
|
|
475
|
+
throw new Error("LayeredRepository requires at least one layer");
|
|
476
|
+
}
|
|
477
|
+
this.layers = opts.layers;
|
|
478
|
+
this.writableIdx = opts.layers.findIndex((l) => !l.readOnly);
|
|
479
|
+
}
|
|
480
|
+
async get(ref) {
|
|
481
|
+
for (const layer of this.layers) {
|
|
482
|
+
const item = await layer.repo.get(ref);
|
|
483
|
+
if (item) return item;
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
async put(ref, spec, opts) {
|
|
488
|
+
if (this.writableIdx < 0) {
|
|
489
|
+
throw new Error("LayeredRepository: no writable layer configured");
|
|
490
|
+
}
|
|
491
|
+
return this.layers[this.writableIdx].repo.put(ref, spec, opts);
|
|
492
|
+
}
|
|
493
|
+
async delete(ref, opts) {
|
|
494
|
+
if (this.writableIdx < 0) {
|
|
495
|
+
throw new Error("LayeredRepository: no writable layer configured");
|
|
496
|
+
}
|
|
497
|
+
return this.layers[this.writableIdx].repo.delete(ref, opts);
|
|
498
|
+
}
|
|
499
|
+
async *list(filter) {
|
|
500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
501
|
+
const limit = filter.limit ?? Infinity;
|
|
502
|
+
let yielded = 0;
|
|
503
|
+
for (const layer of this.layers) {
|
|
504
|
+
for await (const header of layer.repo.list(filter)) {
|
|
505
|
+
const key = refKey(header.ref);
|
|
506
|
+
if (seen.has(key)) continue;
|
|
507
|
+
seen.add(key);
|
|
508
|
+
yield header;
|
|
509
|
+
if (++yielded >= limit) return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async *history(ref, opts = {}) {
|
|
514
|
+
const events = [];
|
|
515
|
+
for (const layer of this.layers) {
|
|
516
|
+
for await (const evt of layer.repo.history(ref, opts)) {
|
|
517
|
+
events.push(tagSource(layer.label, evt));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
events.sort((a, b) => a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0);
|
|
521
|
+
const limit = opts.limit ?? Infinity;
|
|
522
|
+
let yielded = 0;
|
|
523
|
+
for (const evt of events) {
|
|
524
|
+
yield evt;
|
|
525
|
+
if (++yielded >= limit) return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
watch(filter, since) {
|
|
529
|
+
return multiplexWatch(this.layers, filter, since);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
function multiplexWatch(layers, filter, since) {
|
|
533
|
+
return {
|
|
534
|
+
[Symbol.asyncIterator]() {
|
|
535
|
+
const children = layers.map((layer) => ({
|
|
536
|
+
label: layer.label,
|
|
537
|
+
iter: layer.repo.watch(filter, since)[Symbol.asyncIterator](),
|
|
538
|
+
pending: null,
|
|
539
|
+
done: false
|
|
540
|
+
}));
|
|
541
|
+
let closed = false;
|
|
542
|
+
const pumpAll = () => {
|
|
543
|
+
for (const c of children) {
|
|
544
|
+
if (c.done || c.pending) continue;
|
|
545
|
+
c.pending = c.iter.next().then((result) => ({ label: c.label, result }));
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const closeAll = async () => {
|
|
549
|
+
if (closed) return;
|
|
550
|
+
closed = true;
|
|
551
|
+
await Promise.all(
|
|
552
|
+
children.map(async (c) => {
|
|
553
|
+
try {
|
|
554
|
+
await c.iter.return?.(void 0);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
};
|
|
560
|
+
return {
|
|
561
|
+
async next() {
|
|
562
|
+
while (!closed) {
|
|
563
|
+
pumpAll();
|
|
564
|
+
const inflight = children.filter((c) => c.pending);
|
|
565
|
+
if (!inflight.length) return { value: void 0, done: true };
|
|
566
|
+
const winner = await Promise.race(inflight.map((c) => c.pending));
|
|
567
|
+
const target = children.find((c) => c.label === winner.label);
|
|
568
|
+
target.pending = null;
|
|
569
|
+
if (winner.result.done) {
|
|
570
|
+
target.done = true;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
value: tagSource(winner.label, winner.result.value),
|
|
575
|
+
done: false
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return { value: void 0, done: true };
|
|
579
|
+
},
|
|
580
|
+
async return() {
|
|
581
|
+
await closeAll();
|
|
582
|
+
return { value: void 0, done: true };
|
|
583
|
+
},
|
|
584
|
+
async throw(err) {
|
|
585
|
+
await closeAll();
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
export {
|
|
593
|
+
BranchError,
|
|
594
|
+
ConflictError,
|
|
595
|
+
InMemoryRepository,
|
|
596
|
+
LAYER_SOURCE,
|
|
597
|
+
LayeredRepository,
|
|
598
|
+
MetaRefSchema,
|
|
599
|
+
MetadataCache,
|
|
600
|
+
MetadataError,
|
|
601
|
+
MetadataEventSchema,
|
|
602
|
+
MetadataItemSchema,
|
|
603
|
+
MetadataOpSchema,
|
|
604
|
+
MetadataTypeSchema,
|
|
605
|
+
NotFoundError,
|
|
606
|
+
SchemaValidationError,
|
|
607
|
+
canonicalize,
|
|
608
|
+
hashSpec,
|
|
609
|
+
refKey
|
|
610
|
+
};
|
|
611
|
+
//# sourceMappingURL=index.js.map
|