beakerbrowser/webdb
{ "createdAt": "2017-07-25T18:46:27Z", "defaultBranch": "master", "description": "The Web is your database.", "fullName": "beakerbrowser/webdb", "homepage": "https://www.npmjs.com/package/@beaker/webdb", "language": "JavaScript", "name": "webdb", "pushedAt": "2018-12-02T19:30:07Z", "stargazersCount": 398, "topics": [], "updatedAt": "2025-07-23T21:23:07Z", "url": "https://github.com/beakerbrowser/webdb"}A database that reads and writes records on dat:// websites. [How it works]!(#how-it-works)
Example
Section titled “Example”Instantiate:
// in the browserconst WebDB = require('@beaker/webdb')var webdb = new WebDB('webdb-example')
// in nodejsconst DatArchive = require('node-dat-archive')const WebDB = require('@beaker/webdb')var webdb = new WebDB('./webdb-example', {DatArchive})Define your table:
webdb.define('people', { // validate required attributes before indexing validate(record) { assert(record.firstName && typeof record.firstName === 'string') assert(record.lastName && typeof record.lastName === 'string') return true },
// secondary indexes for fast queries (optional) index: ['lastName', 'lastName+firstName', 'age'],
// files to index filePattern: [ '/person.json', '/people/*.json' ]})Then open the DB:
await webdb.open()Next we add archives to be indexed into the database.
await webdb.indexArchive('dat://alice.com')await webdb.indexArchive(['dat://bob.com', 'dat://carla.com'])Now we can begin querying the database for records.
// get any person record where lastName === 'Roberts'var mrRoberts = await webdb.people.get('lastName', 'Roberts')
// response attributes:console.log(mrRoberts.lastName) // => 'Roberts'console.log(mrRoberts) // => {lastName: 'Roberts', ...}console.log(mrRoberts.getRecordURL()) // => 'dat://foo.com/bar.json'console.log(mrRoberts.getRecordOrigin()) // => 'dat://foo.com'console.log(mrRoberts.getIndexedAt()) // => 1511913554723
// get any person record named Bob Robertsvar mrRoberts = await webdb.people.get('lastName+firstName', ['Roberts', 'Bob'])
// get all person records with the 'Roberts' lastnamevar robertsFamily = await webdb.people .where('lastName') .equalsIgnoreCase('roberts') .toArray()
// get all person records with the 'Roberts' lastname// and a firstname that starts with 'B'// - this uses a compound indexvar robertsFamilyWithaBName = await webdb.people .where('lastName+firstName') .between(['Roberts', 'B'], ['Roberts', 'B\uffff']) .toArray()
// get all person records on a given origin// - `:origin` is an auto-generated attributevar personsOnBobsSite = await webdb.people .where(':origin') .equals('dat://bob.com') .toArray()
// get the 30 oldest people indexedvar oldestPeople = await webdb.people .orderBy('age') .reverse() // oldest first .limit(30) .toArray()
// count the # of young peoplevar oldestPeople = await webdb.people .where('age') .belowOrEqual(18) .count()We can also use WebDB to create, modify, and delete records (and their matching files).
// set the recordawait webdb.people.put('dat://bob.com/person.json', { firstName: 'Bob', lastName: 'Roberts', age: 31})
// update the record if it existsawait webdb.people.update('dat://bob.com/person.json', { age: 32})
// update or create the recordawait webdb.people.upsert('dat://bob.com/person.json', { age: 32})
// delete the recordawait webdb.people.delete('dat://bob.com/person.json')
// update the spelling of all Roberts recordsawait webdb.people .where('lastName') .equals('Roberts') .update({lastName: 'Robertos'})
// increment the age of all people under 18var oldestPeople = await webdb.people .where('age') .belowOrEqual(18) .update(record => { record.age = record.age + 1 })
// delete the 30 oldest peoplevar oldestPeople = await webdb.people .orderBy('age') .reverse() // oldest first .limit(30) .delete()Table of Contents
Section titled “Table of Contents”- [How to use WebDB]!(#how-to-use-webdb)
- [Table definitions]!(#table-definitions)
- [Indexing sites]!(#indexing-sites)
- [Creating queries]!(#creating-queries)
- [Applying linear-scan filters]!(#applying-linear-scan-filters)
- [Applying query modifiers]!(#applying-query-modifiers)
- [Executing ‘read’ queries]!(#executing-read-queries)
- [Executing ‘write’ queries]!(#executing-write-queries)
- [Table helper methods]!(#table-helper-methods)
- [Record methods]!(#record-methods)
- [Handling multiple schemas]!(#handling-multiple-schemas)
- [Preprocessing records]!(#preprocessing-records)
- [Serializing records]!(#serializing-records)
- [Using JSON-Schema to validate]!(#using-json-schema-to-validate)
- [Helper tables]!(#helper-tables)
- [Class: WebDB]!(#class-webdb)
- [new WebDB([name, opts])]!(#new-webdbname-opts)
- [WebDB.delete([name])]!(#webdbdeletename)
- [Instance: WebDB]!(#instance-webdb)
- [webdb.open()]!(#webdbopen)
- [webdb.close()]!(#webdbclose)
- [webdb.delete()]!(#webdbdelete)
- [webdb.define(name, definition)]!(#webdbdefinename-definition)
- [webdb.indexArchive(url[, opts])]!(#webdbindexarchiveurl-opts)
- [webdb.unindexArchive(url)]!(#webdbunindexarchiveurl)
- [webdb.indexFile(archive, filepath)]!(#webdbindexfilearchive-filepath)
- [webdb.indexFile(url)]!(#webdbindexfileurl)
- [webdb.unindexFile(archive, filepath)]!(#webdbunindexfilearchive-filepath)
- [webdb.unindexFile(url)]!(#webdbunindexfileurl)
- [webdb.listSources()]!(#webdblistsources)
- [webdb.isSource(url)]!(#webdbissourceurl)
- [Event: ‘open’]!(#event-open)
- [Event: ‘open-failed’]!(#event-open-failed)
- [Event: ‘indexes-reset’]!(#event-indexes-reset)
- [Event: ‘indexes-updated’]!(#event-indexes-updated)
- [Event: ‘source-indexing’]!(#event-source-indexing)
- [Event: ‘source-index-progress’]!(#event-source-index-progress)
- [Event: ‘source-indexed’]!(#event-source-indexed)
- [Event: ‘source-missing’]!(#event-source-missing)
- [Event: ‘source-found’]!(#event-source-found)
- [Event: ‘source-error’]!(#event-source-error)
- [Instance: WebDBTable]!(#instance-webdbtable)
- [table.count()]!(#tablecount)
- [table.delete(url)]!(#tabledeleteurl)
- [table.each(fn)]!(#tableeachfn)
- [table.filter(fn)]!(#tablefilterfn)
- [table.get(url)]!(#tablegeturl)
- [table.get(key, value)]!(#tablegetkey-value)
- [table.isRecordFile(url)]!(#tableisrecordfileurl)
- [table.limit(n)]!(#tablelimitn)
- [table.listRecordFiles(url)]!(#tablelistrecordfilesurl)
- [table.name]!(#tablename)
- [table.offset(n)]!(#tableoffsetn)
- [table.orderBy(key)]!(#tableorderbykey)
- [table.put(url, record)]!(#tableputurl-record)
- [table.query()]!(#tablequery)
- [table.reverse()]!(#tablereverse)
- [table.schema]!(#tableschema)
- [table.toArray()]!(#tabletoarray)
- [table.update(url, updates)]!(#tableupdateurl-updates)
- [table.update(url, fn)]!(#tableupdateurl-fn)
- [table.upsert(url, updates)]!(#tableupserturl-updates)
- [table.upsert(url, fn)]!(#tableupserturl-fn)
- [table.where(key)]!(#tablewherekey)
- [Event: ‘put-record’]!(#event-put-record)
- [Event: ‘del-record’]!(#event-del-record)
- [Instance: WebDBQuery]!(#instance-webdbquery)
- [query.clone()]!(#queryclone)
- [query.count()]!(#querycount)
- [query.delete()]!(#querydelete)
- [query.each(fn)]!(#queryeachfn)
- [query.eachKey(fn)]!(#queryeachkeyfn)
- [query.eachUrl(fn)]!(#queryeachurlfn)
- [query.filter(fn)]!(#queryfilterfn)
- [query.first()]!(#queryfirst)
- [query.keys()]!(#querykeys)
- [query.last()]!(#querylast)
- [query.limit(n)]!(#querylimitn)
- [query.offset(n)]!(#queryoffsetn)
- [query.orderBy(key)]!(#queryorderbykey)
- [query.put(record)]!(#queryputrecord)
- [query.urls()]!(#queryurls)
- [query.reverse()]!(#queryreverse)
- [query.toArray()]!(#querytoarray)
- [query.uniqueKeys()]!(#queryuniquekeys)
- [query.until(fn)]!(#queryuntilfn)
- [query.update(updates)]!(#queryupdateupdates)
- [query.update(fn)]!(#queryupdatefn)
- [query.where(key)]!(#querywherekey)
- [Instance: WebDBWhereClause]!(#instance-webdbwhereclause)
- [where.above(value)]!(#whereabovevalue)
- [where.aboveOrEqual(value)]!(#whereaboveorequalvalue)
- [where.anyOf(values)]!(#whereanyofvalues)
- [where.anyOfIgnoreCase(values)]!(#whereanyofignorecasevalues)
- [where.below(value)]!(#wherebelowvalue)
- [where.belowOrEqual(value)]!(#wherebeloworequalvalue)
- [where.between(lowerValue, upperValue[, options])]!(#wherebetweenlowervalue-uppervalue-options)
- [where.equals(value)]!(#whereequalsvalue)
- [where.equalsIgnoreCase(value)]!(#whereequalsignorecasevalue)
- [where.noneOf(values)]!(#wherenoneofvalues)
- [where.notEqual(value)]!(#wherenotequalvalue)
- [where.startsWith(value)]!(#wherestartswithvalue)
- [where.startsWithAnyOf(values)]!(#wherestartswithanyofvalues)
- [where.startsWithAnyOfIgnoreCase(values)]!(#wherestartswithanyofignorecasevalues)
- [where.startsWithIgnoreCase(value)]!(#wherestartswithignorecasevalue)
- [How it works]!(#how-it-works)
- [Why not put all records in one file?]!(#why-not-put-all-records-in-one-file)
- [Performance]!(#performance)
- [Linkability]!(#linkability)
- [Why not put all records in one file?]!(#why-not-put-all-records-in-one-file)
- [Changelog]!(#changelog)
- [4.1.0]!(#410)
- [4.0.0]!(#400)
- [3.0.0]!(#300)
How to use WebDB
Section titled “How to use WebDB”Table definitions
Section titled “Table definitions”Use the [define()]!(#webdbdefinename-definition) method to define your tables, and then call [webdb.open()]!(#webdbopen) to create them.
Indexing sites
Section titled “Indexing sites”Use [indexArchive()]!(#webdbindexarchiveurl-opts) and [unindexArchive()]!(#webdbunindexarchiveurl) to control which sites will be indexed.
Indexed data will persist in the database until unindexArchive() is called.
However, indexArchive() should always be called on load to get the latest data.
If you only want to index the current state of a site, and do not want to watch for updates, call indexArchive() with the {watch: false} option.
You can index and de-index individual files using [indexFile()]!(#webdbindexfileurl) and [unindexFile()]!(#webdbunindexfileurl).
Creating queries
Section titled “Creating queries”Queries are created with a chained function API.
You can create a query from the table object using [.query()]!(#tablequery), [.where()]!(#tablewherekey), or [.orderBy()]!(#tableorderbykey).
The where() method returns an object with [multiple filter functions that you can use]!(#instance-webdbwhereclause).
var myQuery = webdb.query().where('foo').equals('bar')var myQuery = webdb.where('foo').equals('bar') // equivalentvar myQuery = webdb.where('foo').startsWith('ba')var myQuery = webdb.where('foo').between('bar', 'baz', {includeLower: true, includeUpper: false})Each query has a primary key.
By default, this is the url attribute, but it can be changed using [.where()]!(#querywherekey) or [.orderBy()]!(#queryorderbykey).
In this example, the primary key becomes ‘foo’:
var myQuery = webdb.orderBy('foo')At this time, the primary key must be one of the indexed attributes.
There are 2 indexes created automatically for every record: url and origin.
The other indexes are specified in your table’s [define()]!(#webdbdefinename-definition) call using the index option.
Applying linear-scan filters
Section titled “Applying linear-scan filters”After the primary key index is applied, you can apply additional filters using [filter(fn)]!(#queryfilterfn) and [until(fn)]!(#queryuntilfn).
These methods are called “linear scan” filters because they require each record to be loaded and then filtered out.
(Until stops when it hits the first false response.)
var myQuery = webdb.query() .where('foo').equals('bar') .filter(record => record.beep == 'boop') // additional filterApplying query modifiers
Section titled “Applying query modifiers”You can apply the following modifiers to your query to alter the output:
- [limit(n)]!(#querylimitn)
- [offset(n)]!(#queryoffsetn)
- [reverse()]!(#queryreverse)
Executing ‘read’ queries
Section titled “Executing ‘read’ queries”Once your query has been defined, you can execute and read the results using one of these methods:
- [count()]!(#querycount)
- [each(fn)]!(#queryeachfn)
- [eachKey(fn)]!(#queryeachkeyfn)
- [eachUrl(fn)]!(#queryeachurlfn)
- [first()]!(#queryfirst)
- [keys()]!(#querykeys)
- [last()]!(#querylast)
- [urls()]!(#queryurls)
- [toArray()]!(#querytoarray)
- [uniqueKeys()]!(#queryuniquekeys)
Executing ‘write’ queries
Section titled “Executing ‘write’ queries”Once your query has been defined, you can execute and modify the results using one of these methods:
- [delete()]!(#querydelete)
- [put(record)]!(#queryputrecord)
- [update(updates)]!(#queryupdateupdates)
- [update(fn)]!(#queryupdatefn)
If you try to modify rows in archives that are not writable, WebDB will throw an error.
Table helper methods
Section titled “Table helper methods”The following methods exist on the table object for query reads and writes:
- [table.delete(url)]!(#tabledeleteurl)
- [table.each(fn)]!(#tableeachfn)
- [table.get(url)]!(#tablegeturl)
- [table.get(key, value)]!(#tablegetkey-value)
- [table.put(url, record)]!(#tableputurl-record)
- [table.toArray()]!(#tabletoarray)
- [table.update(url, updates)]!(#tableupdateurl-updates)
- [table.update(url, fn)]!(#tableupdateurl-fn)
- [table.upsert(url, updates)]!(#tableupserturl-updates)
- [table.upsert(url, fn)]!(#tableupserturl-fn)
Record methods
Section titled “Record methods”record.getRecordURL() // => 'dat://foo.com/bar.json'record.getRecordOrigin() // => 'dat://foo.com'record.getIndexedAt() // => 1511913554723Every record is emitted in a wrapper object with the following methods:
getRecordURL()The URL of the record.getRecordOrigin()The URL of the site the record was found on.getIndexedAt()The timestamp of when the record was indexed.
These attributes can be used in indexes with the following IDs:
:url:origin:indexedAt
For instance:
webdb.define('things', { // ... index: [ ':indexedAt', // ordered by time the record was indexed ':origin+createdAt' // ordered by origin and declared create timestamp (a record attribute) ], // ...})await webdb.open()webdb.things.where(':indexedAt').above(Date.now() - ms('1 week'))webdb.things.where(':origin+createdAt').between(['dat://bob.com', 0], ['dat://bob.com', Infinity])Handling multiple schemas
Section titled “Handling multiple schemas”Since the Web is a complex place, you’ll frequently have to deal with multiple schemas which are slightly different. To deal with this, you can use a definition object to support multiple attribute names under one index.
webdb.define('places', { // ...
index: [ // a simple index definition: 'name',
// an object index definition: {name: 'zipCode', def: ['zipCode', 'zip_code']} ]
// ...})Now, when you run queries on the 'zipCode' key, you will search against both 'zipCode' and 'zip_code'.
Note, however, that the records emitted from the query will not be changed by WebDB and so they may differ.
For example:
webdb.places.where('zipCode').equals('78705').each(record => { console.log(record.zipCode) // may be '78705' or undefined console.log(record.zip_code) // may be '78705' or undefined})To solve this, you can [preprocess records]!(#preprocessing-records).
Preprocessing records
Section titled “Preprocessing records”Sometimes, you need to modify records before they’re stored in the database. This can be for a number of reasons:
- Normalization. Small differences in accepted record schemas may need to be merged (see [handling multiple schemas]!(#handling-multiple-schemas)).
- Indexing. WebDB’s index spec only supports toplevel attributes. If the data is embedded in a sub-object, you’ll need to place the data at the top-level.
- Computed attributes.
For these cases, you can use the preprocess(record) function in the table definition:
webdb.define('places', { // ...
preprocess(record) { // normalize zipCode and zip_code if (record.zip_code) { record.zipCode = record.zip_code }
// move an attribute to the root object for indexing record.title = record.info.title
// compute an attribute record.location = `${record.address} ${record.city}, ${record.state} ${record.zipCode}`
return record }
// ...})These attributes will be stored in the WebDB table.
Serializing records
Section titled “Serializing records”When records are updated by WebDB, they are published to a Dat site as a file. Since these files are distributed on the Web, it’s wise to avoid adding noise to the record.
To control the exact record that will be published, you can set the serialize(record) function in the table definition:
webdb.define('places', { // ...
serialize(record) { // write the following object to the dat site: return { info: record.info, city: record.city, state: record.state, zipCode: record.zipCode } }
// ...})Using JSON-Schema to validate
Section titled “Using JSON-Schema to validate”The default way to validate records is to provide a validator function. If the function throws an error or returns falsy, the record will not be indexed.
It can be tedious to write validation functions, so you might want to use JSON-Schema:
const Ajv = require('ajv')webdb.define('people', { validate: (new Ajv()).compile({ type: 'object', properties: { firstName: { type: 'string' }, lastName: { type: 'string' }, age: { description: 'Age in years', type: 'integer', minimum: 0 } }, required: ['firstName', 'lastName'] }),
// ...})Helper tables
Section titled “Helper tables”Sometimes you need internal storage to help you maintain application state. This may be for interfaces, or data which is private, or for special kinds of indexes.
For instance, in the Fritter app, we needed an index for notifications. This index was conditional: it needed to contain posts which were replies to the user, or likes which were on the user’s post. For cases like this, you can use a “helper table.”
webdb.define('notifications', { helperTable: true, index: ['createdAt'], preprocess (record) { record.createdAt = Date.now() }})When the helperTable attribute is set to true in a table definition, the table will not be used to index Dat archives.
Instead, it will exist purely in the local data cache, and only contain data which is .put() there.
In all other respects, it behaves like a normal table.
In Fritter, the helper table is used with events to track notifications:
// track reply notificationswebdb.posts.on('put', async ({record, url, origin}) => { if (origin === userUrl) return // dont index the user's own posts if (isAReplyToUser(record) === false) return // only index replies to the user if (await isNotificationIndexed(url)) return // don't index if already indexed await db.notifications.put(url, {type: 'reply', url})})webdb.posts.on('del', async ({url}) => { if (await isNotificationIndexed(url)) { await db.notifications.delete(url) }})Class: WebDB
Section titled “Class: WebDB”new WebDB([name, opts])
Section titled “new WebDB([name, opts])”var webdb = new WebDB('mydb')nameString. Defaults to'webdb'. If run in the browser, this will be the name of the IndexedDB instance. If run in NodeJS, this will be the path of the LevelDB folder.optsObject.DatArchiveConstructor. The class constructor for dat archive instances. If in node, you should specify node-dat-archive.
Create a new WebDB instance.
The given name will control where the indexes are saved.
You can specify different names to run multiple WebDB instances at once.
WebDB.delete([name])
Section titled “WebDB.delete([name])”await WebDB.delete('mydb')nameString. Defaults to'webdb'. If run in the browser, this will be the name of the IndexedDB instance. If run in NodeJS, this will be the path of the LevelDB folder.- Returns Promise<Void>.
Deletes the indexes and metadata for the given WebDB.
Instance: WebDB
Section titled “Instance: WebDB”webdb.open()
Section titled “webdb.open()”await webdb.open()- Returns Promise<Object>.
rebuildsArray<String>. The tables which were built or rebuilt during setup.
Runs final setup for the WebDB instance.
This must be run after [.define()]!(#webdbdefinename-definition) to create the table instances.
webdb.close()
Section titled “webdb.close()”await webdb.close()- Returns Promise<Void>.
Closes the WebDB instance.
webdb.delete()
Section titled “webdb.delete()”await webdb.delete()- Returns Promise<Void>.
Closes and destroys all indexes in the WebDB instance.
You can .delete() and then .open() a WebDB to recreate its indexes.
await webdb.delete()await webdb.open()webdb.define(name, definition)
Section titled “webdb.define(name, definition)”nameString. The name of the table.definitionObject.indexArray<String or Object>. A list of attributes which should have secondary indexes produced for querying. Eachindexvalue is a keypath (see https://www.w3.org/TR/IndexedDB/#dfn-key-path) or an object definition (see below).filePatternString or Array<String>. An anymatch list of files to index.helperTableBoolean. If true, the table will be used for storing internal data and will not be used to index Dat archives. See [helper tables]!(#helper-tables)validateFunction. A method to accept or reject a file from indexing based on its content. If the method returns falsy or throws an error, the file will not be indexed.recordObject.- Returns Boolean.
preprocessFunction. A method to modify the record after read from the dat site. See [preprocessing records]!(#preprocessing-records).recordObject.- Returns Object.
serializeFunction. A method to modify the record before write to the dat site. See [serializing records]!(#serializing-records).recordObject.- Returns Object.
- Returns Void.
Creates a new table on the webdb object.
The table will be set at webdb.{name} and be the WebDBTable type.
This method must be called before [open()]!(#webdbopen)
Indexed attributes may either be defined as a keypath string or an object definition. The object definition has the following values:
nameString. The name of the index.defString or Array<String>. The definition of the index.
If the value of def is an array, it supports each definition.
This is useful when supporting multiple schemas ([learn more here]!(#handling-multiple-schemas)).
In the index definition, you can specify compound indexes with a + separator in the keypath.
You can also index each value of an array using the * sigil at the start of the name.
Some example index definitions:
a simple index - 'firstName'as an object def - {name: 'firstName', def: 'firstName'}a compound index - 'firstName+lastName'index an array's values - '*favoriteFruits'many keys - {name: 'firstName', def: ['firstName', 'first_name']}many keys, compound - {name: 'firstName+lastName', def: ['firstName+lastName', 'first_name+last_name']}You can specify which files should be processed into the table using the filePattern option.
If unspecified, it will default to all json files on the site ('*.json').
Example:
webdb.define('people', { validate(record) { assert(record.firstName && typeof record.firstName === 'string') assert(record.lastName && typeof record.lastName === 'string') return true }, index: ['lastName', 'lastName+firstName', 'age'], filePattern: [ '/person.json', '/people/*.json' ]})
await webdb.open()// the new table will now be defined at webdb.peoplewebdb.indexArchive(url[, opts])
Section titled “webdb.indexArchive(url[, opts])”await webdb.indexArchive('dat://foo.com')urlString or DatArchive or Array<String or DatArchive>. The sites to index.optsObject.watchBoolean. Should WebDB watch the archive for changes, and index them immediately? Defaults to true.
- Returns Promise<Void>.
Add one or more dat:// sites to be indexed. The method will return when the site has been fully indexed. This will add the given archive to the “sources” list.
webdb.unindexArchive(url)
Section titled “webdb.unindexArchive(url)”await webdb.unindexArchive('dat://foo.com')urlString or DatArchive. The site to deindex.- Returns Promise<Void>.
Remove a dat:// site from the dataset. The method will return when the site has been fully de-indexed. This will remove the given archive from the “sources” list.
webdb.indexFile(archive, filepath)
Section titled “webdb.indexFile(archive, filepath)”await webdb.indexFile(fooArchive, '/bar.json')archiveDatArchive. The site containing the file to index.filepathString. The path of the file to index.- Returns Promise<Void>.
Add a single file to the index. The method will return when the file has been indexed.
This will not add the file or its archive to the “sources” list.
Unlike indexArchive, WebDB will not watch the file after this call.
webdb.indexFile(url)
Section titled “webdb.indexFile(url)”await webdb.indexFile('dat://foo.com/bar.json')urlString. The url of the file to index.- Returns Promise<Void>.
Add a single file to the index. The method will return when the file has been indexed.
This will not add the file or its archive to the “sources” list.
webdb.unindexFile(archive, filepath)
Section titled “webdb.unindexFile(archive, filepath)”await webdb.unindexFile(fooArchive, '/bar.json')archiveDatArchive. The site containing the file to deindex.filepathString. The path of the file to deindex.- Returns Promise<Void>.
Remove a single file from the dataset. The method will return when the file has been de-indexed.
webdb.unindexFile(url)
Section titled “webdb.unindexFile(url)”await webdb.unindexFile('dat://foo.com')urlString. The url of the file to deindex.- Returns Promise<Void>.
Remove a single file from the dataset. The method will return when the file has been de-indexed.
webdb.listSources()
Section titled “webdb.listSources()”var urls = await webdb.listSources()- Returns Array<String>.
Lists the URLs of the dat:// sites which are included in the dataset.
webdb.isSource(url)
Section titled “webdb.isSource(url)”var urls = await webdb.isSource('dat://foo.com')- Returns Boolean.
Is the given dat:// URL included in the dataset?
Event: ‘open’
Section titled “Event: ‘open’”webdb.on('open', () => { console.log('WebDB is ready for use')})Emitted when the WebDB instance has been opened using [open()]!(#webdbopen).
Event: ‘open-failed’
Section titled “Event: ‘open-failed’”webdb.on('open-failed', (err) => { console.log('WebDB failed to open', err)})errorError.
Emitted when the WebDB instance fails to open during [open()]!(#webdbopen).
Event: ‘indexes-reset’
Section titled “Event: ‘indexes-reset’”webdb.on('indexes-reset', () => { console.log('WebDB detected a change in schemas and reset all indexes')})Emitted when the WebDB instance detects a change in the schemas and has to reindex the dataset. All indexes are cleared and will be reindexed as sources are added.
Event: ‘indexes-updated’
Section titled “Event: ‘indexes-updated’”webdb.on('indexes-updated', (url, version) => { console.log('Tables were updated for', url, 'at version', version)})urlString. The archive that was updated.versionNumber. The version which was updated to.
Emitted when the WebDB instance has updated the stored data for a archive.
Event: ‘source-indexing’
Section titled “Event: ‘source-indexing’”webdb.on('source-indexing', (url, startVersion, targetVersion) => { console.log('Tables are updating for', url, 'from version', startVersion, 'to', targetVersion)})urlString. The archive that was updated.startVersionNumber. The version which is being indexed from.targetVersionNumber. The version which is being indexed to.
Emitted when the WebDB instance has started to index the given archive.
Event: ‘source-index-progress’
Section titled “Event: ‘source-index-progress’”webdb.on('source-index-progress', (url, tick, total) => { console.log('Update for', url, 'is', Math.round(tick / total * 100), '% complete')})urlString. The archive that was updated.tickNumber. The current update being applied.totalNumber. The total number of updates being applied.
Emitted when an update has been applied during an indexing process.
Event: ‘source-indexed’
Section titled “Event: ‘source-indexed’”webdb.on('source-indexed', (url, version) => { console.log('Tables were updated for', url, 'at version', version)})urlString. The archive that was updated.versionNumber. The version which was updated to.
Emitted when the WebDB instance has indexed the given archive.
This is similar to 'indexes-updated', but it fires every time a source is indexed, whether or not it results in updates to the indexes.
Event: ‘source-missing’
Section titled “Event: ‘source-missing’”webdb.on('source-missing', (url) => { console.log('WebDB couldnt find', url, '- now searching')})Emitted when a source’s data was not locally available or found on the network.
When this occurs, WebDB will continue searching for the data, and emit 'source-found' on success.
Event: ‘source-found’
Section titled “Event: ‘source-found’”webdb.on('source-found', (url) => { console.log('WebDB has found and indexed', url)})Emitted when a source’s data was found after originally not being found during indexing.
This event will only be emitted after 'source-missing' is emitted.
Event: ‘source-error’
Section titled “Event: ‘source-error’”webdb.on('source-error', (url, err) => { console.log('WebDB failed to index', url, err)})Emitted when a source fails to load.
Instance: WebDBTable
Section titled “Instance: WebDBTable”table.count()
Section titled “table.count()”var numRecords = await webdb.mytable.count()- Returns Promise<Number>.
Count the number of records in the table.
table.delete(url)
Section titled “table.delete(url)”await webdb.mytable.delete('dat://foo.com/bar.json')- Returns Promise<Number>. The number of deleted records (should be 0 or 1).
Delete the record at the given URL.
table.each(fn)
Section titled “table.each(fn)”await webdb.mytable.each(record => { console.log(record)})fnFunction.recordObject.- Returns Void.
- Returns Promise<Void>.
Iterate over all records in the table with the given function.
table.filter(fn)
Section titled “table.filter(fn)”var records = await webdb.mytable.filter(record => { return (record.foo == 'bar')})fnFunction.recordObject.- Returns Boolean.
- Returns WebDBQuery.
Start a new query and apply the given filter function to the resultset.
table.get(url)
Section titled “table.get(url)”var record = await webdb.mytable.get('dat://foo.com/myrecord.json')urlString. The URL of the record to fetch.- Returns Promise<Object>.
Get the record at the given URL.
table.get(key, value)
Section titled “table.get(key, value)”var record = await webdb.mytable.get('foo', 'bar')keyString. The keyname to search against.valueAny. The value to match against.- Promise<Object>.
Get the record first record to match the given key/value query.
table.isRecordFile(url)
Section titled “table.isRecordFile(url)”var isRecord = webdb.mytable.isRecordFile('dat://foo.com/myrecord.json')urlString.- Returns Boolean.
Tells you whether the given URL matches the table’s file pattern.
table.limit(n)
Section titled “table.limit(n)”var query = webdb.mytable.limit(10)nNumber.- Returns WebDBQuery.
Creates a new query with the given limit applied.
table.listRecordFiles(url)
Section titled “table.listRecordFiles(url)”var recordFiles = await webdb.mytable.listRecordFiles('dat://foo.com')urlString.- Returns Promise<Array<Object>>. On each object:
recordUrlString.tableWebDBTable.
Lists all files on the given URL which match the table’s file pattern.
table.name
Section titled “table.name”- String.
The name of the table.
table.offset(n)
Section titled “table.offset(n)”var query = webdb.mytable.offset(5)nNumber.- Returns WebDBQuery.
Creates a new query with the given offset applied.
table.orderBy(key)
Section titled “table.orderBy(key)”var query = webdb.mytable.orderBy('foo')keyString.- Returns WebDBQuery.
Creates a new query ordered by the given key.
table.put(url, record)
Section titled “table.put(url, record)”await webdb.mytable.put('dat://foo.com/myrecord.json', {foo: 'bar'})urlString.recordObject.- Returns Promise<String>. The URL of the written record.
Replaces or creates the record at the given URL with the record.
table.query()
Section titled “table.query()”var query = webdb.mytable.query()- Returns WebDBQuery.
Creates a new query.
table.reverse()
Section titled “table.reverse()”var query = webdb.mytable.reverse()- Returns WebDBQuery.
Creates a new query with reverse-order applied.
table.schema
Section titled “table.schema”- Object.
The schema definition for the table.
table.toArray()
Section titled “table.toArray()”var records = await webdb.mytable.toArray()- Returns Promise<Array<Object>>.
Returns an array of all records in the table.
table.update(url, updates)
Section titled “table.update(url, updates)”var wasUpdated = await webdb.mytable.update('dat://foo.com/myrecord.json', {foo: 'bar'})urlString. The record to update.updatesObject. The new values to set on the record.- Returns Promise<Number>. The number of records updated.
Updates the target record with the given key values, if it exists.
table.update(url, fn)
Section titled “table.update(url, fn)”var wasUpdated = await webdb.mytable.update('dat://foo.com/myrecord.json', record => { record.foo = 'bar' return record})urlString. The record to update.fnFunction. A method to modify the record.recordObject. The record to modify.- Returns Object.
- Returns Promise<Number>. The number of records updated.
Updates the target record with the given function, if it exists.
table.upsert(url, updates)
Section titled “table.upsert(url, updates)”var didCreateNew = await webdb.mytable.upsert('dat://foo.com/myrecord.json', {foo: 'bar'})urlString. The record to update.updatesObject. The new values to set on the record.- Returns Promise<Number>. The number of records updated.
If a record exists at the target URL, will update it with the given key values. If a record does not exist, will create the record.
table.upsert(url, fn)
Section titled “table.upsert(url, fn)”var didCreateNew = await webdb.mytable.upsert('dat://foo.com/myrecord.json', record => { if (record) { // update record.foo = 'bar' return record } // create return {foo: 'bar'}})urlString. The record to update.fnFunction. A method to modify the record.recordObject. The record to modify. Will be falsy if the record does ot previously exist- Returns Object.
- Returns Promise<Number>. The number of records updated.
Updates the target record with the given function, if it exists. If a record does not exist, will give a falsy value to the method.
table.where(key)
Section titled “table.where(key)”var whereClause = webdb.mytable.where('foo')keyString.- Returns WebDBWhereClause.
Creates a new where-clause using the given key.
Event: ‘put-record’
Section titled “Event: ‘put-record’”webdb.mytable.on('put-record', ({url, origin, indexedAt, record}) => { console.log('Table was updated for', url, '(origin:', origin, ') at ', indexedAt) console.log('Record data:', record)})urlString. The url of the record that was updated.originString. The url origin of the record that was updated.indexedAtNumber. The timestamp of the index update.recordObject. The content of the updated record.
Emitted when the table has updated the stored data for a record.
Event: ‘del-record’
Section titled “Event: ‘del-record’”webdb.mytable.on('del-record', ({url, origin, indexedAt}) => { console.log('Table was updated for', url, '(origin:', origin, ') at ', indexedAt)})urlString. The url of the record that was deleted.originString. The url origin of the record that was deleted.indexedAtNumber. The timestamp of the index update.
Emitted when the table has deleted the stored data for a record. This can happen because the record has been deleted, or because a new version of the record fails validation.
Instance: WebDBQuery
Section titled “Instance: WebDBQuery”query.clone()
Section titled “query.clone()”var query = webdb.mytable.query().clone()- Returns WebDBQuery.
Creates a copy of the query.
query.count()
Section titled “query.count()”var numRecords = await webdb.mytable.query().count()- Returns Promise<Number>. The number of found records.
Gives the count of records which match the query.
query.delete()
Section titled “query.delete()”var numDeleted = await webdb.mytable.query().delete()- Returns Promise<Number>. The number of deleted records.
Deletes all records which match the query.
query.each(fn)
Section titled “query.each(fn)”await webdb.mytable.query().each(record => { console.log(record)})fnFunction.recordObject.- Returns Void.
- Returns Promise<Void>.
Calls the given function with all records which match the query.
query.eachKey(fn)
Section titled “query.eachKey(fn)”await webdb.mytable.query().eachKey(url => { console.log('URL =', url)})fnFunction.keyString.- Returns Void.
- Returns Promise<Void>.
Calls the given function with the value of the query’s primary key for each matching record.
The key is determined by the index being used.
By default, this is the url attribute, but it can be changed by using where() or orderBy().
Example:
await webdb.mytable.orderBy('age').eachKey(age => { console.log('Age =', age)})query.eachUrl(fn)
Section titled “query.eachUrl(fn)”await webdb.mytable.query().eachUrl(url => { console.log('URL =', url)})fnFunction.urlString.- Returns Void.
- Returns Promise<Void>.
Calls the given function with the URL of each matching record.
query.filter(fn)
Section titled “query.filter(fn)”var query = webdb.mytable.query().filter(record => { return record.foo == 'bar'})fnFunction.recordObject.- Returns Boolean.
- Returns WebDBQuery.
Applies an additional filter on the query.
query.first()
Section titled “query.first()”var record = await webdb.mytable.query().first()- Returns Promise<Object>.
Returns the first result in the query.
query.keys()
Section titled “query.keys()”var keys = await webdb.mytable.query().keys()- Returns Promise<Array<String>>.
Returns the value of the primary key for each matching record.
The key is determined by the index being used.
By default, this is the url attribute, but it can be changed by using where() or orderBy().
var ages = await webdb.mytable.orderBy('age').keys()query.last()
Section titled “query.last()”var record = await webdb.mytable.query().last()- Returns Promise<Object>.
Returns the last result in the query.
query.limit(n)
Section titled “query.limit(n)”var query = webdb.mytable.query().limit(10)nNumber.- Returns WebDBQuery.
Limits the number of matching record to the given number.
query.offset(n)
Section titled “query.offset(n)”var query = webdb.mytable.query().offset(10)nNumber.- Returns WebDBQuery.
Skips the given number of matching records.
query.orderBy(key)
Section titled “query.orderBy(key)”var query = webdb.mytable.query().orderBy('foo')keyString.- Returns WebDBQuery.
Sets the primary key and sets the resulting order to match its values.
query.put(record)
Section titled “query.put(record)”var numWritten = await webdb.mytable.query().put({foo: 'bar'})recordObject.- Returns Promise<Number>. The number of written records.
Replaces each matching record with the given value.
query.urls()
Section titled “query.urls()”var urls = await webdb.mytable.query().urls()- Returns Promise<Array<String>>.
Returns the url of each matching record.
query.reverse()
Section titled “query.reverse()”var query = webdb.mytable.query().reverse()- Returns WebDBQuery.
Reverses the order of the results.
query.toArray()
Section titled “query.toArray()”var records = await webdb.mytable.query().toArray()- Returns Promise<Array<Object>>.
Returns the value of each matching record.
query.uniqueKeys()
Section titled “query.uniqueKeys()”var keys = await webdb.mytable.query().uniqueKeys()- Returns Promise<Array<String>>.
Returns the value of the primary key for each matching record, with duplicates filtered out.
The key is determined by the index being used.
By default, this is the url attribute, but it can be changed by using where() or orderBy().
Example:
var ages = await webdb.mytable.orderBy('age').uniqueKeys()query.until(fn)
Section titled “query.until(fn)”var query = webdb.mytable.query().until(record => { return record.foo == 'bar'})fnFunction.recordObject.- Returns Boolean.
- Returns WebDBQuery.
Stops emitting matching records when the given function returns true.
query.update(updates)
Section titled “query.update(updates)”var numUpdated = await webdb.mytable.query().update({foo: 'bar'})updatesObject. The new values to set on the record.- Returns Promise<Number>. The number of updated records.
Updates all matching record with the given values.
query.update(fn)
Section titled “query.update(fn)”var numUpdated = await webdb.mytable.query().update(record => { record.foo = 'bar' return record})fnFunction. A method to modify the record.recordObject. The record to modify.- Returns Object.
- Returns Promise<Number>. The number of updated records.
Updates all matching record with the given function.
query.where(key)
Section titled “query.where(key)”var whereClause = webdb.mytable.query().where('foo')keyString. The attribute to query against.- Returns WebDBWhereClause.
Creates a new where clause.
Instance: WebDBWhereClause
Section titled “Instance: WebDBWhereClause”where.above(value)
Section titled “where.above(value)”var query = webdb.mytable.query().where('foo').above('bar')var query = webdb.mytable.query().where('age').above(18)valueAny. The lower bound of the query.- Returns WebDBQuery.
where.aboveOrEqual(value)
Section titled “where.aboveOrEqual(value)”var query = webdb.mytable.query().where('foo').aboveOrEqual('bar')var query = webdb.mytable.query().where('age').aboveOrEqual(18)valueAny. The lower bound of the query.- Returns WebDBQuery.
where.anyOf(values)
Section titled “where.anyOf(values)”var query = webdb.mytable.query().where('foo').anyOf(['bar', 'baz'])valuesArray<Any>.- Returns WebDBQuery.
Does not work on compound indexes.
where.anyOfIgnoreCase(values)
Section titled “where.anyOfIgnoreCase(values)”var query = webdb.mytable.query().where('foo').anyOfIgnoreCase(['bar', 'baz'])valuesArray<Any>.- Returns WebDBQuery.
Does not work on compound indexes.
where.below(value)
Section titled “where.below(value)”var query = webdb.mytable.query().where('foo').below('bar')var query = webdb.mytable.query().where('age').below(18)valueAny. The upper bound of the query.- Returns WebDBQuery.
where.belowOrEqual(value)
Section titled “where.belowOrEqual(value)”var query = webdb.mytable.query().where('foo').belowOrEqual('bar')var query = webdb.mytable.query().where('age').belowOrEqual(18)valueAny. The upper bound of the query.- Returns WebDBQuery.
where.between(lowerValue, upperValue[, options])
Section titled “where.between(lowerValue, upperValue[, options])”var query = webdb.mytable.query().where('foo').between('bar', 'baz', {includeUpper: true, includeLower: true})var query = webdb.mytable.query().where('age').between(18, 55, {includeLower: true})lowerValueAny.upperValueAny.optionsObject.includeUpperBoolean.includeLowerBoolean.
- Returns WebDBQuery.
where.equals(value)
Section titled “where.equals(value)”var query = webdb.mytable.query().where('foo').equals('bar')valueAny.- Returns WebDBQuery.
where.equalsIgnoreCase(value)
Section titled “where.equalsIgnoreCase(value)”var query = webdb.mytable.query().where('foo').equalsIgnoreCase('bar')valueAny.- Returns WebDBQuery.
Does not work on compound indexes.
where.noneOf(values)
Section titled “where.noneOf(values)”var query = webdb.mytable.query().where('foo').noneOf(['bar', 'baz'])valuesArray<Any>.- Returns WebDBQuery.
Does not work on compound indexes.
where.notEqual(value)
Section titled “where.notEqual(value)”var query = webdb.mytable.query().where('foo').notEqual('bar')valueAny.- Returns WebDBQuery.
Does not work on compound indexes.
where.startsWith(value)
Section titled “where.startsWith(value)”var query = webdb.mytable.query().where('foo').startsWith('ba')valueAny.- Returns WebDBQuery.
Does not work on compound indexes.
where.startsWithAnyOf(values)
Section titled “where.startsWithAnyOf(values)”var query = webdb.mytable.query().where('foo').startsWithAnyOf(['ba', 'bu'])valuesArray<Any>.- Returns WebDBQuery.
Does not work on compound indexes.
where.startsWithAnyOfIgnoreCase(values)
Section titled “where.startsWithAnyOfIgnoreCase(values)”var query = webdb.mytable.query().where('foo').startsWithAnyOfIgnoreCase(['ba', 'bu'])valuesArray<Any>.- Returns WebDBQuery.
Does not work on compound indexes.
where.startsWithIgnoreCase(value)
Section titled “where.startsWithIgnoreCase(value)”var query = webdb.mytable.query().where('foo').startsWithIgnoreCase('ba')valueAny.- Returns WebDBQuery.
Does not work on compound indexes.
How it works
Section titled “How it works”WebDB abstracts over the DatArchive API to provide a simple database-like interface. It’s inspired by Dexie.js and built using LevelDB. (In the browser, it runs on IndexedDB using level.js.
WebDB scans a set of source Dat archives for files that match a path pattern. Web DB caches and indexes those files so they can be queried easily and quickly. WebDB also provides a simple interface for adding, editing, and removing records from archives.
WebDB sits on top of Dat archives. It duplicates ingested data into IndexedDB, which acts as a throwaway cache. The cached data can be reconstructed at any time from the source Dat archives.
WebDB treats individual files in the Dat archive as individual records in a table. As a result, there’s a direct mapping for each table to a folder of JSON files. For instance, if you had a posts table, it might map to the /posts/*.json files. WebDB’s mutators, e.g., put, add, update, simply writes records as JSON files in the posts/ directory. WebDB’s readers and query-ers, like get() and where(), read from the IndexedDB cache.
WebDB watches its source archives for changes to the JSON files that compose its records. When the files change, it syncs and reads the changes, then updates IndexedDB, keeping query results up-to-date. Roughly, the flow is: put() -> archive/posts/12345.json -> indexer -> indexeddb -> get().
Why not put all records in one file?
Section titled “Why not put all records in one file?”Storing records in one file—posts.json for example—is an intuitive way to manage data on the peer-to-peer Web, but putting each record in an individual file is a much better choice for performance and linkability.
Performance
Section titled “Performance”The dat:// protocol doesn’t support partial updates at the file-level, which means that with multiple records in a single file, every time a user adds a record, anyone who follows that user must sync and re-download the entire file. As the file continues to grow, performance will degrade. Putting each record in an individual file is much more efficient: when a record is created, peers in the network will only download the newly-created file.
Linkability
Section titled “Linkability”Putting each record in an individual file also makes each record linkable! This isn’t as important as performance, but it’s a nice feature to have. See Dog Legs McBoot’s status update as an example:
dat://232ac2ce8ad4ed80bd1b6de4cbea7d7b0cad1441fa62312c57a6088394717e41/posts/0jbdviucy.jsonChangelog
Section titled “Changelog”A quick overview of the notable changes to WebDB:
Added “helper tables,” which make it possible to track private state and build more sophisticated indexes.
Replaced JSON-Schema validation with an open validate function. This was done to reduce the output bundle size (by 200kb!) and to improve overall flexibility (JSON-Schema and JSON-LD do not work together very well).
The addSource() and removeSource() methods were replaced with indexArchive(), indexFile(), unindexArchive(), and unindexFile().
The indexArchive() method also provides an option to disable watching.
This change was made as we found controlling the index was an important part of using WebDB. Frequently we’d want to index an archive temporarily, for instance to view a user’s profile on first visit.
This new API gives better control for those use-cases, and no longer assumes you want to continue watching an archive after indexing it once.