@dreamlake/dreamdb 0.5.1 → 0.5.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.
@@ -37,6 +37,42 @@ class AuthoringUnavailable {
37
37
 
38
38
  export { AuthoringUnavailable as Authoring }
39
39
 
40
+ /**
41
+ * `Writer.ingestCmaf` is Node-only, for the same reason `Authoring` is: it is
42
+ * gated behind the `write-full` feature, and the browser build is compiled
43
+ * with `write`.
44
+ *
45
+ * The shared declarations describe it on both ends (see build.sh, "types"), so
46
+ * a browser caller type-checks and would otherwise reach
47
+ * `writer.ingestCmaf is not a function` at runtime — a message that names
48
+ * neither the cause nor the fix. This replaces it with one that does.
49
+ *
50
+ * Applied to the prototype rather than declared as a method because `Writer`
51
+ * comes from the wasm module; there is no class here to extend.
52
+ */
53
+ function installIngestCmafStub(Writer) {
54
+ if (!Writer || typeof Writer !== 'function') return Writer
55
+ if (typeof Writer.prototype.ingestCmaf === 'function') return Writer // full build
56
+ Object.defineProperty(Writer.prototype, 'ingestCmaf', {
57
+ configurable: true,
58
+ writable: true,
59
+ value: async function ingestCmaf() {
60
+ throw new Error(
61
+ 'Writer.ingestCmaf is not available in the browser build of ' +
62
+ '@dreamlake/dreamdb. CMAF ingest takes the whole clip as one ' +
63
+ 'argument, so peak memory is the entire source — and fragmenting a ' +
64
+ 'multi-GB file inside a tab\'s 32-bit address space is not a ' +
65
+ 'workflow this package pretends to support. Run it under Node (the ' +
66
+ '"node" export condition resolves to the full build), or fragment ' +
67
+ 'ahead of time and append the fragments.',
68
+ )
69
+ },
70
+ })
71
+ return Writer
72
+ }
73
+
74
+ export { installIngestCmafStub }
75
+
40
76
  /**
41
77
  * A Backend (contract v2) that gets bytes over plain `fetch` and writes through
42
78
  * short-lived presigned URLs minted by *your* server.
@@ -86,7 +122,7 @@ export class PresignedBackend {
86
122
  * @param {object} o
87
123
  * @param {string} o.readBase Base URL for reads.
88
124
  * @param {(paths: string[], opts?: {ifMatch?: string}) => Promise<Record<string, {url: string, headers?: Record<string,string>}>>} o.mintPut
89
- * @param {(path: string, opts: {ifMatch?: string, ifNoneMatchStar?: boolean}, bytes: Uint8Array) => Promise<{status: string, etag?: string}>} [o.commitRef]
125
+ * @param {(path: string, opts: {ifMatch?: string, ifNoneMatchStar?: boolean}, bytes: Uint8Array) => Promise<{outcome?: string, status?: string, etag?: string}>} [o.commitRef]
90
126
  * Server-side ref commit. Used when `refStrategy` is `'proxy'`.
91
127
  * @param {'proxy'|'presign'} [o.refStrategy='proxy']
92
128
  * How the 33-byte ref object is written. `'proxy'` posts it to your
@@ -119,15 +155,27 @@ export class PresignedBackend {
119
155
  return this._readUrl ? this._readUrl(path) : `${this.readBase}/${path}`
120
156
  }
121
157
 
122
- /** v2 get: returns `{bytes, etag}`. Range is inclusive of `end`. */
158
+ /** v2 get: returns `{bytes, etag, totalLength}`. Range is half-open `[start, end)`. */
123
159
  async get(path, range) {
124
160
  const headers = {}
125
- if (range) headers.range = `bytes=${range.start}-${range.end}`
161
+ if (range) {
162
+ if (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) ||
163
+ range.start < 0 || range.end <= range.start) {
164
+ throw new Error(`GET ${path}: invalid half-open range [${range.start}, ${range.end})`)
165
+ }
166
+ // HTTP uses an inclusive end; the Backend contract uses an exclusive one.
167
+ headers.range = `bytes=${range.start}-${range.end - 1}`
168
+ }
126
169
  const r = await fetch(this.#url(path), { headers })
127
170
  if (!r.ok) throw new Error(`GET ${path}: ${r.status} ${r.statusText}`)
171
+ const contentRange = r.headers.get('content-range')
172
+ const rangeTotal = contentRange && /^bytes \d+-\d+\/(\d+)$/.exec(contentRange)?.[1]
173
+ const lengthText = range ? rangeTotal : r.headers.get('content-length')
174
+ const totalLength = lengthText == null ? undefined : Number(lengthText)
128
175
  return {
129
176
  bytes: new Uint8Array(await r.arrayBuffer()),
130
177
  etag: normalizeEtag(r.headers.get('etag')),
178
+ totalLength: Number.isSafeInteger(totalLength) && totalLength >= 0 ? totalLength : undefined,
131
179
  }
132
180
  }
133
181
 
@@ -148,7 +196,12 @@ export class PresignedBackend {
148
196
  // semantics differ per provider — so they get their own path.
149
197
  const isRef = path.startsWith('refs/')
150
198
  if (isRef && this.refStrategy === 'proxy') {
151
- return await this.commitRef(path, opts, bytes)
199
+ // `commitRef` is the CALLER's callback and its documented contract is the
200
+ // `{status}` string form, so it is normalised here rather than being
201
+ // broken. This is the ref path — the one where mistaking a refused CAS
202
+ // for a successful one loses a write — so it does not rely on the
203
+ // adapter's compatibility handling.
204
+ return normalizePutResult(await this.commitRef(path, opts, bytes), path)
152
205
  }
153
206
 
154
207
  const minted = await this.mintPut([path], opts.ifMatch ? { ifMatch: opts.ifMatch } : undefined)
@@ -167,11 +220,11 @@ export class PresignedBackend {
167
220
  // someone else moved it, which is a conflict the caller must resolve.
168
221
  if (r.status === 412 || r.status === 409) {
169
222
  return opts.ifNoneMatchStar
170
- ? { status: 'exists' }
171
- : { status: 'casFailed', etag: normalizeEtag(r.headers.get('etag')) }
223
+ ? { outcome: 'alreadyExists' }
224
+ : { outcome: 'casFailed', etag: normalizeEtag(r.headers.get('etag')) }
172
225
  }
173
226
  if (!r.ok) throw new Error(`PUT ${path}: ${r.status} ${r.statusText}`)
174
- return { status: 'created', etag: normalizeEtag(r.headers.get('etag')) }
227
+ return { outcome: 'created', etag: normalizeEtag(r.headers.get('etag')) }
175
228
  }
176
229
 
177
230
  // No `delete`. It would need a signed DELETE URL, and the sign endpoint only
@@ -181,6 +234,28 @@ export class PresignedBackend {
181
234
  // dropping a ref is an `Authoring` operation, which is Node-only.
182
235
  }
183
236
 
237
+ /**
238
+ * Normalise a caller-supplied put result into the tagged form.
239
+ *
240
+ * Accepts the documented `{status: 'created'|'exists'|'casFailed'}` and the
241
+ * tagged `{outcome}`; anything else is refused by name rather than silently
242
+ * treated as success, because the one thing a ref write must never do is report
243
+ * a refused CAS as a completed one.
244
+ */
245
+ function normalizePutResult(r, path) {
246
+ if (r && typeof r.outcome === 'string') return r
247
+ const s = r && r.status
248
+ if (s === 'created') return { outcome: 'created', etag: r.etag }
249
+ if (s === 'exists' || s === 'alreadyExists') return { outcome: 'alreadyExists', etag: r.etag }
250
+ if (s === 'casFailed') return { outcome: 'casFailed', etag: r.etag }
251
+ if (s === 'notFound') return { outcome: 'notFound' }
252
+ if (typeof s === 'number') return r
253
+ throw new Error(
254
+ `commitRef(${path}) resolved an unrecognised result; expected ` +
255
+ `{outcome} or {status: 'created'|'exists'|'casFailed'}, got ${JSON.stringify(r)}`,
256
+ )
257
+ }
258
+
184
259
  /**
185
260
  * S3 quotes ETags and sometimes suffixes them (`W/"…"`, `"…-3"` for multipart).
186
261
  * The conditional-write comparison is byte-exact, so a stray quote turns every
package/browser.mjs CHANGED
@@ -2,3 +2,10 @@
2
2
  // module and instantiated by the bundler; there is nothing to await.
3
3
  export * from './browser/dreamdb.js'
4
4
  export { ZERO_SPATIAL_KEY, PresignedBackend, Authoring } from './browser-extras.mjs'
5
+
6
+ // `ingestCmaf` is behind `write-full`, so it is absent here while the shared
7
+ // declarations describe it. Replace the bare `is not a function` with a
8
+ // message that names the reason and the fix.
9
+ import { Writer as _W } from './browser/dreamdb.js'
10
+ import { installIngestCmafStub as _stub } from './browser-extras.mjs'
11
+ _stub(_W)
package/index.d.ts CHANGED
@@ -23,6 +23,14 @@ export class Authoring {
23
23
  private constructor();
24
24
  free(): void;
25
25
  [Symbol.dispose](): void;
26
+ /**
27
+ * Add one fixed-shape typed-array Constant.
28
+ *
29
+ * `field` is the same `{name, kind:'array', ...}` descriptor accepted by
30
+ * `create`, with `trackKind:'constant'` and `objectKind:'constant'`.
31
+ * `payload` is a raw or NPY payload matching that declaration.
32
+ */
33
+ addArrayConstant(field: any, payload: Uint8Array): Promise<void>;
26
34
  /**
27
35
  * Add an embedding column whose index artifacts already exist.
28
36
  *
@@ -127,6 +135,15 @@ export class S3Backend {
127
135
  * content is hash-addressed; refs are read fresh each page load anyway).
128
136
  */
129
137
  get(path: string, _opts: any): Promise<Uint8Array>;
138
+ /**
139
+ * Stream `path` without first aggregating the response in wasm memory.
140
+ *
141
+ * Returning the length beside the browser stream satisfies the optional
142
+ * `Backend.getStream` contract. Older injected backends that implement
143
+ * only [`Self::get`] remain compatible, but cannot claim bounded memory
144
+ * for historical oversized inline Track reads.
145
+ */
146
+ getStream(path: string): Promise<any>;
130
147
  /**
131
148
  * List object paths under `prefix`. Stub (returns empty); the current
132
149
  * consumer read paths don't require listing.
@@ -142,6 +159,32 @@ export class Space {
142
159
  private constructor();
143
160
  free(): void;
144
161
  [Symbol.dispose](): void;
162
+ /**
163
+ * The Space's human-readable description, or `undefined` when none is set.
164
+ *
165
+ * The read half of `Authoring.setDescription` (#129). It lives here rather
166
+ * than on `Authoring` because that type is `write-full` and so exists only
167
+ * in the Node build — a description is metadata a READER wants, and
168
+ * `Space` is the surface all three build targets share.
169
+ *
170
+ * `async` although `Dataset::description` is not: the value comes from the
171
+ * Dataset this Space opens lazily at the Manifest hash already pinned by
172
+ * this Space, so the first call may perform that open without re-resolving
173
+ * a source Ref.
174
+ *
175
+ * Deliberately routed through `Dataset::description` rather than read out
176
+ * of `self.manifest`'s registry, which would be shorter and would also
177
+ * serve the cached decoded Manifest. `Dataset::recover_meta` is not a prefix
178
+ * filter: it accepts the legacy `vortex.*` namespace as well as
179
+ * `dreamdb.*`, canonicalises, and drops structural keys. Matching
180
+ * `dreamdb.description` here would silently return nothing for a legacy
181
+ * Space whose description is on disk and readable from Rust — the exact
182
+ * defect `recover_meta`'s own comment records.
183
+ *
184
+ * `Option<String>` marshals to `string | undefined`, NOT to `""`. An unset
185
+ * description and an empty one stay distinguishable across the boundary.
186
+ */
187
+ description(): Promise<string | undefined>;
145
188
  /**
146
189
  * Open a Space from a `.../refs/<name>` or `.../manifests/<hash>` URI,
147
190
  * resolving the ref (if any) and loading the manifest. If `backend` is
@@ -221,18 +264,55 @@ export class Space {
221
264
  * two-pass rerank + HotShard merge + tombstone suppression.
222
265
  */
223
266
  queryVector(field: string, query: Float32Array, opts: any): Promise<Array<any>>;
267
+ /**
268
+ * Read an Event/Unbucketed or Continuous/TimeBatch typed-array field as
269
+ * `Map<anchorString, {values, shape, ...}>`.
270
+ */
271
+ readArrayColumn(field: string): Promise<Map<any, any>>;
272
+ /**
273
+ * Read a typed-array Constant and decode it to a JS TypedArray plus its
274
+ * authoritative shape and semantic metadata.
275
+ */
276
+ readArrayConstant(field: string): Promise<any>;
224
277
  /**
225
278
  * Read a scalar track as a `Map<anchorString, value>`. Single-anchor
226
279
  * entries resolve from the track index; multi-anchor entries fetch (and
227
280
  * de-dupe) their bucket and roaring-decode the anchor set.
228
281
  */
229
282
  readScalarColumn(track: any, _opts: any): Promise<Map<any, any>>;
283
+ /**
284
+ * Fetch verified initialization and complete Fragments overlapping one
285
+ * item-relative half-open range. This materializes the selected bytes; it
286
+ * is not a streaming API.
287
+ */
288
+ readVideoItemRange(field: string, itemKey: Uint8Array, relativeStartNs: bigint | number, relativeEndNs: bigint | number): Promise<VideoItemRead | undefined>;
230
289
  /**
231
290
  * Fetch a track object and return its `object_index` (array of CBOR
232
291
  * entries). Inline indexes only — paged indexes are not resolved (matching
233
292
  * dreamdb-ts, which also doesn't support them on this path).
234
293
  */
235
294
  resolveObjectIndex(track: any): Promise<Array<any>>;
295
+ /**
296
+ * Semantic-cache capacity in bytes (spec/0006 §3.6). Default 1 GiB.
297
+ */
298
+ semanticCacheCapacity(): Promise<number>;
299
+ /**
300
+ * Semantic-cache occupancy as `{ entries, bytesUsed, maxBytes }`.
301
+ */
302
+ semanticCacheStats(): Promise<object>;
303
+ /**
304
+ * Set the semantic-cache capacity in bytes, evicting down to it before
305
+ * returning. Returns the bytes evicted. `0` disables the cache.
306
+ *
307
+ * An index whose accounted size exceeds the cap is not cached and is
308
+ * re-fetched on every query, reported honestly as a miss. The default is
309
+ * not raised globally to suit one corpus.
310
+ *
311
+ * Sizing is workload- and architecture-dependent, and figures measured on
312
+ * a 64-bit host do NOT carry over here: `usize` is 32 bits on `wasm32`, so
313
+ * every offset and length inside a decoded index accounts differently.
314
+ */
315
+ setSemanticCacheCapacity(n_bytes: number): Promise<number>;
236
316
  /**
237
317
  * Base32 hash of the manifest's first timeline.
238
318
  */
@@ -245,6 +325,16 @@ export class Space {
245
325
  * List resolved tracks from this manifest.
246
326
  */
247
327
  tracks(): Array<any>;
328
+ /**
329
+ * Look up the logical VideoItem containing one absolute Timeline anchor
330
+ * without fetching media. Exact nanosecond values are returned as bigint.
331
+ */
332
+ videoItemAt(field: string, anchorNs: bigint | number): Promise<VideoItemInfo | undefined>;
333
+ /**
334
+ * Look up one logical VideoItem by its stable opaque key without fetching
335
+ * initialization or Fragment bytes.
336
+ */
337
+ videoItemByKey(field: string, itemKey: Uint8Array): Promise<VideoItemInfo | undefined>;
248
338
  /**
249
339
  * The base URL objects are fetched relative to (consumers build their own
250
340
  * object URLs from this — e.g. `dreamdb-demo`'s track readers).
@@ -289,6 +379,50 @@ export class Writer {
289
379
  * Tombstone records by anchor. Returns the new manifest hash.
290
380
  */
291
381
  deleteRecords(anchors: BigUint64Array, reason?: string | null): Promise<string>;
382
+ /**
383
+ * Ingest a pre-fragmented CMAF (fragmented-MP4) video into a Video field
384
+ * as a `spec/0007` §5 media Track — the thing that makes a video Track
385
+ * time-seekable and MSE-playable rather than one opaque blob.
386
+ *
387
+ * ```js
388
+ * const out = await writer.ingestCmaf('video', initBytes, [
389
+ * [frag0, 0n, 2_000_000_000n],
390
+ * [frag1, 2_000_000_000n, 4_000_000_000n],
391
+ * ])
392
+ * ```
393
+ *
394
+ * - `init` is the initialization segment (`ftyp`+`moov`).
395
+ * - each fragment is `[Uint8Array, tStartNs, tEndNs]`, half-open, already
396
+ * offset to the timeline anchor the caller wants. Pass `bigint`s:
397
+ * wall-clock nanosecond anchors are far past `Number.MAX_SAFE_INTEGER`
398
+ * and a rounded `tStart` writes the fragment into a 60 s bucket that
399
+ * does not match the address it is indexed at.
400
+ *
401
+ * Calling it again on the SAME field APPENDS (one episode per call on a
402
+ * shared timeline) and reuses the track's existing init segment; a clip
403
+ * whose init differs — different codec, resolution, fps or audio config —
404
+ * is refused, because a Track carries exactly one init segment and the
405
+ * mismatch would produce fragments that decode against the wrong one.
406
+ *
407
+ * Returns `{field, modality, initHash, fragmentCount, coverage: [bigint,
408
+ * bigint], trackHash, manifestHash}`. `coverage` is `bigint` for the
409
+ * reason above; `fragmentCount` is the track's total after the append,
410
+ * not the number passed in.
411
+ *
412
+ * **Node only** (`write-full`). This takes every fragment's bytes as an
413
+ * argument, so peak memory is the whole clip; the native path form streams
414
+ * fragment-by-fragment off disk, which wasm32 cannot do because it has no
415
+ * filesystem. Fragmenting a multi-GB source inside a browser tab's 32-bit
416
+ * address space is not a workflow worth pretending to support.
417
+ */
418
+ /**
419
+ * **Node only.** Gated behind the `write-full` feature, which the browser
420
+ * and web builds are not compiled with: CMAF ingest takes the whole clip as
421
+ * one argument, so peak memory is the entire source. Calling it in a browser
422
+ * throws with an explanation — see the stub in browser-extras.mjs. Typed on
423
+ * both ends, like `Authoring`, so shared code type-checks once.
424
+ */
425
+ ingestCmaf(field: string, init: Uint8Array, fragments: CmafFragment[]): Promise<CmafIngestResult>;
292
426
  /**
293
427
  * Open a ref for writing.
294
428
  *
@@ -428,6 +562,13 @@ export function timeAnchorHex(value: bigint): string;
428
562
  */
429
563
  export function timeBucket(t_start: bigint, duration: string): bigint;
430
564
 
565
+ /**
566
+ * Decode one spec/0025 item using the same declaration and payload path as
567
+ * `Space.readArrayColumn`, without requiring storage IO. This is the SDK's
568
+ * pure conformance entry point for language-neutral vectors.
569
+ */
570
+ export function typedArrayDecode(item_type: any, payload: Uint8Array): any;
571
+
431
572
  /**
432
573
  * Package version — useful for consumers to confirm which build is loaded.
433
574
  */
@@ -444,6 +585,62 @@ export function zeroSpatialKey(): string;
444
585
  /** Zero spatial-key path segment for non-spatial (fragment) object addresses. */
445
586
  export const ZERO_SPATIAL_KEY: string
446
587
 
588
+ /**
589
+ * One CMAF fragment for {@link Writer.ingestCmaf}: `[bytes, tStartNs, tEndNs)`,
590
+ * half-open, already offset to the timeline anchor you want.
591
+ *
592
+ * Pass `bigint`s. A `number` is accepted only while it is an exact
593
+ * non-negative integer at or below `Number.MAX_SAFE_INTEGER` and is rejected
594
+ * above it rather than rounded — wall-clock nanosecond anchors are ~1.7e18, and
595
+ * a rounded `tStart` files the fragment under a different 60 s bucket than the
596
+ * address it is indexed at.
597
+ */
598
+ export type CmafFragment = [bytes: Uint8Array, tStartNs: bigint | number, tEndNs: bigint | number]
599
+
600
+ /** What {@link Writer.ingestCmaf} resolves to. */
601
+ export interface CmafIngestResult {
602
+ /** Schema field this Track materializes. */
603
+ field: string
604
+ /** Modality string, e.g. `video.h264`. */
605
+ modality: string
606
+ /** Base32 multihash of the initialization-segment object. */
607
+ initHash: string
608
+ /** Fragments in the Track *after* this call — not the number passed in. */
609
+ fragmentCount: number
610
+ /** Half-open `[tMin, tMax)` coverage of the whole Track, in ns. */
611
+ coverage: [bigint, bigint]
612
+ /** Base32 multihash of the new Track object. */
613
+ trackHash: string
614
+ /** Base32 multihash of the manifest this ingest published. */
615
+ manifestHash: string
616
+ }
617
+
618
+ /** Stable metadata for one logical VideoItem. All times are exact bigint ns. */
619
+ export interface VideoItemInfo {
620
+ modality: string
621
+ itemKey: Uint8Array
622
+ tStartNs: bigint
623
+ durationNs: bigint
624
+ itemAddress: string
625
+ initAddress: string
626
+ }
627
+
628
+ /** One complete Fragment selected by {@link Space.readVideoItemRange}. */
629
+ export interface VideoItemFragmentRead {
630
+ tStartNs: bigint
631
+ tEndNs: bigint
632
+ bytes: Uint8Array
633
+ }
634
+
635
+ /** Materialized initialization plus complete Fragments for one relative range. */
636
+ export interface VideoItemRead {
637
+ item: VideoItemInfo
638
+ rangeStartNs: bigint
639
+ rangeEndNs: bigint
640
+ init: Uint8Array
641
+ fragments: VideoItemFragmentRead[]
642
+ }
643
+
447
644
  /** A resolved manifest track (shape returned by `Space.tracks()`). */
448
645
  export interface ResolvedTrack {
449
646
  modality: string
@@ -475,11 +672,24 @@ export type PutStatus = 'created' | 'exists' | 'casFailed'
475
672
  * CORS rule; without it the browser cannot read the header at all.
476
673
  */
477
674
  export interface Backend {
478
- /** Inclusive byte range. Returning a bare Uint8Array is the accepted v1 shape. */
675
+ /** Half-open byte range `[start, end)`. Returning a bare Uint8Array is the accepted whole-object v1 shape. */
479
676
  get(
480
677
  path: string,
481
678
  range?: { start: number; end: number },
482
- ): Promise<Uint8Array | { bytes: Uint8Array; etag?: string }>
679
+ ): Promise<Uint8Array | { bytes: Uint8Array; etag?: string; totalLength?: number }>
680
+
681
+ /**
682
+ * Optional bounded full-object stream. Without this method reads remain
683
+ * functional, but oversized historical inline Tracks use the unbounded v1
684
+ * compatibility path.
685
+ */
686
+ getStream?(
687
+ path: string,
688
+ ): Promise<ReadableStream<Uint8Array> | {
689
+ stream: ReadableStream<Uint8Array>
690
+ etag?: string
691
+ totalLength?: number
692
+ }>
483
693
 
484
694
  head?(path: string): Promise<{ exists?: boolean; etag?: string; size?: number }>
485
695