@stele-climbing/stele1-directory-handle
v0.0.2
Published
browser interface for stele1 catalogs over any FileSystemDirectoryHandle (File System Access picker or OPFS)
Maintainers
Readme
@stele-climbing/stele1-directory-handle
Browser interface for stele1 catalogs,
backed by any FileSystemDirectoryHandle —
from the File System Access picker or from OPFS.
A Catalog reads and writes a catalog directory as described by the
stele1 spec:
one JSON file per record,
photo images alongside their records,
metadata in stele1.json at the root.
All methods are async.
Iterators are async generators,
consumed with for await.
This package is ESM-only.
Getting a directory handle
// File System Access picker (user-chosen directory; Chromium, secure context)
const handle = await window.showDirectoryPicker({ mode: 'readwrite' });
// OPFS (origin-private, broadly supported including Firefox/Safari)
const handle = await navigator.storage.getDirectory();The catalog takes an already-permissioned handle; it does not prompt.
Use
import { Catalog } from '@stele-climbing/stele1-directory-handle';
const catalog = Catalog.plain(handle);
if (!(await catalog.exists())) await catalog.create();
await catalog.setClimb({
uuid: '550e8400-e29b-41d4-a716-446655440000',
name: 'The Mandala',
rating: { difficulty: 'V12' },
});
for await (const climb of catalog.eachClimb()) {
console.log(climb.name);
}Each datatype (climb, area, photo, trail, parking) has matching methods:
{type}Uuids()lists the uuids of stored recordsget{Type}(uuid)reads one record (rejects on a missing file, invalid JSON, or a uuid that doesn't match its filename)set{Type}(record)writes one record, keyed by itsuuideach{Type}(onError)lazily iterates parsed records; a file that fails to read or parse is skipped, andonError(err, uuid)is called if provided
Catalog metadata (stele1.json) is read and written with
getMetadata() and setMetadata(metadata).
Creating a catalog
await catalog.create(); // mints a catalog uuid, lays down dirs
await catalog.create({ 'stele1.txt': introText }); // with custom asset contentscreate refuses a directory that already contains anything.
Unlike the Node backend there are no bundled asset files in the browser,
so create takes asset contents as an argument,
with brief stub texts as the default.
Deleting records
Trails and parking delete directly:
dropTrail(uuid), dropParking(uuid).
Photos can be referenced by nothing,
so dropPhoto(uuid) deletes the record and its image file.
Climbs and areas can be referenced by photo layers, so deletion is explicit about what happens to those references:
purgeClimb(uuid)removes the climb and strips it from every photo'sclimb_layers.dropClimbNoPurge(uuid)removes only the record, leaving any photo layers dangling.
purgeArea / dropAreaNoPurge mirror this for areas.
Renderers should tolerate a layer whose reference no longer resolves.
Photo images
await catalog.setPhotoImage(uuid, blob, 'jpg'); // store a Blob or File
await catalog.getPhotoImageBlob(uuid); // File (a Blob), or nullImages are stored as {uuid}_image.{ext} next to the photo's JSON.
Setting a new image removes the old one,
including one with a different extension.
Adapters
A Catalog doesn't interpret records itself;
an adapter supplies parse, stringify, and getUuid per record type
(metadata needs only parse and stringify).
Catalog.plain(handle) uses the built-in plain adapter:
records are plain objects,
no validation,
output formatted with sorted keys and stable indentation so catalogs diff cleanly under version control.
To validate records on read and write, use the adapter shipped by @stele-climbing/stele1-datatypes:
import { Catalog } from '@stele-climbing/stele1-directory-handle';
import { adapter } from '@stele-climbing/stele1-datatypes/adapter';
const catalog = new Catalog(handle, adapter);
// eachClimb() now yields Climb instances, validated on read;
// setClimb accepts them, validated on writeAdapters travel across backends: the same adapter works with the Node filesystem package, and all shipped adapters produce identically formatted output, so a catalog moves between environments unchanged and diffs cleanly whichever backend wrote it. The backends share the on-disk format and the adapter contract, not their APIs; each is idiomatic for its environment.
Persistence (OPFS)
OPFS is best-effort by default and may be evicted under disk pressure. Request durable storage once if the catalog should be kept:
await navigator.storage.persist();Requirements
- A secure context (HTTPS or
localhost). - The File System Access picker is Chromium-only; OPFS is broadly supported.
showDirectoryPickermust be called from a user gesture.
Install
npm install @stele-climbing/stele1-directory-handleDevelopment
Source and issues live at codeberg.org/stele-climbing/stele1.
License
MIT
