Record
Records are one of deepstream's core features. A Record is an arbitrary JSON data structure that can be created, retrieved, updated, deleted and listened to. Records are created and retrieved using client.record.getRecord('name')
To learn more about how they are used, have a look at the Record Tutorial.
Creating records​
Records are created and retrieved using client.record.getRecord( 'name' );
const recordName = `user/${client.getUid()}` // "user/iqaphzxy-2o1pnsvcnbo"
const record = client.record.getRecord(recordName)
info
By creating/retrieving the record, the client will automatically receive all record updates made by other clients.
Properties​
| Argument | Type | Description |
|---|---|---|
| name | String | The name of the record, as specified when calling client.record.getRecord( name ) |
| isReady | Boolean | True once the record has received its current data and emitted the 'ready' event |
| hasProvider | Boolean | True once a listener accepts subscriptions to a record. Otherwise there are no active listeners. The 'hasProviderChanged' event is proving the information whenever the values has been changed. |
| isDestroyed | Boolean | True once the record has been discarded or deleted. The record would need to be retrieved again via `client.record.getRecord( name ) |
Events​
hasProviderChanged​
Emitted whenever the hasProvider property has been changed. Argument is the hasProvider property.
delete​
Emitted when the record was deleted, whether by this client or by another.
discard​
Emitted once the record was discarded.
error​
Emitted if the record encounters an error. The error message is passed to the event callback.
Methods​
whenReady(callback? | Promise)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| callback | Function | true | A function that should be invoked as soon as the record is ready. |
Immediately executes the callback if the record is ready. Otherwise, it registers it as a callback for the ready event.
// Callback
record.whenReady(record => {
// data has now been loaded
})
// ES6
await record.whenReady()
set(path, value, callback?)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| path | String | true | A particular path within the JSON structure that should be set |
| value | Various | false | The value the record or path should be set to |
| callback | Function | true | Will be called with the result of the write when using record write acknowledgements |
Used to set the record's data and can be called with a value. A path and callback can optionally be included.
Including a callback will indicate that write acknowledgement to cache or storage is required and will slow down the operation.
info
After calling set, you still have to wait for the record to be ready before a get call will return the value assigned by set.
// Set the entire record's data
record.set({
personalData: {
firstname: 'Homer',
lastname: 'Simpson',
status: 'married'
},
children: ['Bart', 'Maggie', 'Lisa']
});
// Update only firstname
record.set('personalData.firstname', 'Marge')
// Set the entire record with write acknowledgement
record.set({
personalData: { ... },
children: [ ... ]
}, err => {
if (err) {
console.log('Record set with error:', err)
} else {
console.log('Record set without error')
}
});
// Update only a property with write acknowledgement
record.set('personalData.firstname', 'Homer', err => {
if (err) {
console.log('Record set with error:', err)
} else {
console.log('Record set without error')
}
})
Forbidden paths​
Since server v10.0.5, paths containing any of __proto__, constructor, or prototype are rejected by the server to prevent prototype-pollution attacks on the cached record state. Examples of paths that are now refused:
__proto__
constructor.prototype.toString
nested.__proto__.polluted
arr[0].constructor
When such a path is sent by a client (via set, setWithAck, setMulti, or any path-bearing record write), the server responds with INVALID_MESSAGE_DATA for that write and the record state is left unchanged. The same validation now also runs inside Valve permission rule evaluation, so a forbidden path will surface as a permission/validation failure rather than reaching cache or storage. If you need to store keys named like that, namespace them — e.g. meta.constructor becomes meta.userConstructor.
Write error notification feature​
Starting with deepstream v6, there is slight change in the set logic that allows faster operations with write error notification. If writing to cache or storage fails, a RECORD_UPDATE_ERROR error message will be forwarded to the record instance that can be listened to and thus manage the write error from the client, without having to explicitely wait for the write acknowledgement. Before deepstream v6 if such error ocurred the client was not aware of it.
const record = client.record.getRecord('test')
// set record data without write ack
record.set({ data: 'ok' })
record.set('path', 5)
// record error listener
record.on('error', (e) => {
if (e === 'RECORD_UPDATE_ERROR') {
// write to database or cache failed
// handle it properly: retry or nuke the operation...
}
})
note
If RECORD_UPDATE_ERROR is emitted, all pending operations with write acknowledgement will receive the error message callback. This is due to the fact that a write error could potentially corrupt data and thus leave the record instance out of sync with the database.
setWithAck(path, value)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| path | String | true | A particular path within the JSON structure that should be set |
| value | Various | false | The value the record or path should be set to |
Used to set the record's data and can be called with a value. A path can optionally be included.
This function returns a promise that fulfills when writing to cache or storage completed thus slowing down the operation.
info
After awaiting setWithAck, the data is persisted so using get will retrieve the updated record.
// Set the entire record's data with write acknowledgement
await record.setWithAck({
personalData: {
firstname: 'Homer',
lastname: 'Simpson',
status: 'married'
},
children: ['Bart', 'Maggie', 'Lisa']
});
// Update only firstname with write acknowledgement
await record.setWithAck('personalData.firstname', 'Marge')
setMulti(patches, callback?)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| patches | Array<{path, data}> | false | An ordered list of {path, data} operations to apply atomically. Setting data: undefined on an entry erases that path. |
| callback | Function | true | Will be called with the result of the write when using record write acknowledgements |
Applies an ordered batch of path updates as a single message on the wire — one action, one server-side version bump, one cache and storage write. Either all of the patches are applied, or none of them are; there is no observable intermediate state.
setMulti exists so that compound mutations (for example, updating several related fields, or maintaining the linked-list pointers inside a Dequeue) can be performed atomically without multiple back-to-back set calls:
- Version races. Each
setbumps the version and competes independently with other writers. A batchedsetMultionly opens one race window. - Partial application on conflict. If a second writer wins the race halfway through your sequence of
setcalls, you can be left with a half-applied state.setMultiis rejected as a whole or applied as a whole. - Ability to ack a multi-step operation. with
setMulti, the single callback covers the entire batch.
// Update three related fields in one round trip, one version bump
record.setMulti([
{ path: 'personalData.firstname', data: 'Marge' },
{ path: 'personalData.status', data: 'married' },
{ path: 'children', data: ['Bart', 'Maggie', 'Lisa'] }
])
// Mix sets and erases by passing data: undefined
record.setMulti([
{ path: 'personalData.firstname', data: 'Homer' },
{ path: 'personalData.middleName', data: undefined } // erases the path
])
// With a write-ack callback
record.setMulti(
[
{ path: 'a', data: 1 },
{ path: 'b', data: 2 }
],
err => {
if (err) console.log('setMulti failed:', err)
}
)
info
setMulti requires a server version >= 10.1 and client version >= 7.1. When talking to an older server the write is rejected with INVALID_MESSAGE_DATA. Use setMultiWithAck (or pass a callback) if you need to detect this and fall back to setEntries or a sequence of set calls.
setMultiWithAck(patches)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| patches | Array<{path, data}> | false | An ordered list of {path, data} operations to apply atomically |
Same as setMulti, but returns a promise that resolves when writing to cache or storage completes, and rejects with the server error otherwise.
try {
await record.setMultiWithAck([
{ path: 'a', data: 1 },
{ path: 'b', data: 2 }
])
} catch (err) {
// err === 'INVALID_MESSAGE_DATA' on an old server, or a storage error
}
get(path)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| path | String | true | A particular path within the JSON structure that should be retrieved. |
Used to return the record's data but if called without an argument, will return all the data. get() can also be used to retrive a specific part by defining a path string. If the path can not be found, undefined will be returned.
record.get() // Returns entire object
record.get('children[1]') // 'Maggie'
record.get('personalData.firstname') // 'Homer'
subscribe(path, callback, triggerNow)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| path | String | true | A path within the JSON structure that should be subscribed to. |
| callback | Function | false | A function that is called whenever the value changes and the data passed through. |
| triggerNow | Boolean | true | If true, the callback function will be called immediately with the current value. |
Registers a function will be called whenever the record's value changes. All of the record's data can be subscribed to by providing a callback function or when changes are performed to a specific path within the record.
Optional: Passing true will execute the callback immediately with the record's current value.
info
Subscribe is an operation done per record instance. Each time you call client.getRecord(name) you can subscribe and then unsubscribe to that specific record instance.
Listening to any changes on the record:
// Subscribe to any change of the record
function userDataChanged(data) {
// do stuff...
}
user.subscribe(userDataChanged)
Listening to changes on a specific path within the record:
// Only subscribe to the status of the user
function statusChanged( status ) {
if (status === 'married') {
// I want my childhood back!
}
}
user.subscribe('status', statusChanged, true)
unsubscribe(path, callback)​
| Argument | Type | Optional | Description |
|---|---|---|---|
| path | String | true | The path that was previously used for subscribe. |
| callback | Function | false | The previously registered callback function. |
Removes a subscription previous made using record.subscribe(). Defining a path with unsubscribe removes that specific path, or with a callback, can remove it from generic subscriptions.
info
unsubscribe is entirely a client-side operation. To notify the server that the app would no longer interested in the record, and thus do not receive more data updates, use discard() instead.
Unsubscribe all callbacks registered with the path status:
user.unsubscribe('status')
Unsubscribe a specific callback registered for the path status:
user.unsubscribe('status', statusChanged)
Unsubscribe a specific callback registered for the record:
user.unsubscribe(userDataChanged)
Unsubscribe all callbacks not associated with a path:
user.unsubscribe()
discard()​
Removes all change listeners and notifies the server that client no longer wants updates for this record instance.
user.discard()
Make sure to avoid race conditions, there is a recordDiscardTimeout option that will define the number of milliseconds before actually executing the discard operation.
info
It is important to use this operation for record instances that are no longer needed in order to remove listeners.
delete()​
This permanently deletes the record on the server for all users.
user.delete()
Make sure to avoid race conditions, there is a recordDeleteTimeout option that will define the number of milliseconds before actually executing the delete operation.
info
Since deleting a record means that it no longer exists, the resulting action will be a forced discard to all clients with that record. Creating a record directly after deleting it without waiting for the delete event can end up in a race condition. Try to ensure the record has been deleted succesfully to avoid edge cases.
erase(path: string)​
Deletes a path from the record. Equivalent to doing record.set(path, undefined)
user.erase('name')
eraseWithAck(path: string, callback? | Promise)​
Deletes a path from the record and either takes a callback that will be called when the write has been done or returns a promise that will resolve when the write is done.