Skip to content

Commit a2f1d76

Browse files
authored
Merge pull request #931 from streamich/log-metadata
Log metadata
2 parents 1bfe642 + a6b4dcc commit a2f1d76

File tree

5 files changed

+38
-13
lines changed

5 files changed

+38
-13
lines changed

src/json-crdt/log/Log.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import type {JsonNode} from '../nodes/types';
3434
*
3535
* @todo Make this implement UILifecycle (start, stop) interface.
3636
*/
37-
export class Log<N extends JsonNode = JsonNode<any>> implements Printable {
37+
export class Log<N extends JsonNode = JsonNode<any>, Metadata extends Record<string, unknown> = Record<string, unknown>>
38+
implements Printable
39+
{
3840
/**
3941
* Creates a `PatchLog` instance from a newly JSON CRDT model. Checks if
4042
* the model API buffer has any initial operations applied, if yes, it
@@ -60,6 +62,13 @@ export class Log<N extends JsonNode = JsonNode<any>> implements Printable {
6062
return new Log<N>(beginning, model);
6163
}
6264

65+
/**
66+
* Custom metadata associated with the log, it will be stored in the log's
67+
* header when serialized with {@link LogEncoder} and can be used to store
68+
* additional information about the log.
69+
*/
70+
public metadata: Metadata;
71+
6372
/**
6473
* The collection of patches which are applied to the `start()` model to reach
6574
* the `end` model. The patches in the log, stored in an AVL tree for
@@ -94,6 +103,8 @@ export class Log<N extends JsonNode = JsonNode<any>> implements Printable {
94103
* @readonly
95104
*/
96105
public readonly end: Model<N> = start(),
106+
107+
metadata?: Metadata,
97108
) {
98109
const onPatch = (patch: Patch) => {
99110
const id = patch.getId();
@@ -103,6 +114,7 @@ export class Log<N extends JsonNode = JsonNode<any>> implements Printable {
103114
const api = end.api;
104115
this.__onPatch = api.onPatch.listen(onPatch);
105116
this.__onFlush = api.onFlush.listen(onPatch);
117+
this.metadata = metadata ?? ({} as Metadata);
106118
}
107119

108120
/**

src/json-crdt/log/codec/LogDecoder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class LogDecoder {
7070
}
7171

7272
public deserialize(components: types.LogComponentsWithFrontier, params: DeserializeParams = {}): DecodeResult {
73-
const [view, metadata, model, , ...frontier] = components;
73+
const [view, header, model, , ...frontier] = components;
7474
const result: DecodeResult = {};
7575
if (params.view) result.view = view;
7676
if (params.history) result.history = this.deserializeHistory(components);
@@ -79,7 +79,7 @@ export class LogDecoder {
7979
if (result.history) {
8080
result.frontier = result.history;
8181
} else if (model) {
82-
const modelFormat = metadata[1];
82+
const modelFormat = header[1];
8383
const start = (): Model => {
8484
const isSidecar = modelFormat === FileModelEncoding.SidecarBinary;
8585
if (isSidecar) {
@@ -102,7 +102,7 @@ export class LogDecoder {
102102
}
103103

104104
public deserializeHistory(components: types.LogComponentsWithFrontier): Log {
105-
const [, , , history, ...frontier] = components;
105+
const [, header, , history, ...frontier] = components;
106106
const [startSerialized] = history;
107107
const start = (): Model => {
108108
if (!history || !startSerialized) {
@@ -112,7 +112,7 @@ export class LogDecoder {
112112
}
113113
return this.deserializeModel(startSerialized);
114114
};
115-
const log = new Log(start);
115+
const log = new Log(start, void 0, header[0]);
116116
const end = log.end;
117117
if (history) {
118118
const [, patches] = history;

src/json-crdt/log/codec/LogEncoder.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ export class LogEncoder {
2424

2525
public serialize(log: Log, params: SerializeParams = {}): types.LogComponents {
2626
if (params.noView && params.model === 'sidecar') throw new Error('SIDECAR_MODEL_WITHOUT_VIEW');
27-
const metadata: types.LogMetadata = [{}, FileModelEncoding.Auto];
27+
const header: types.LogHeader = [log.metadata ?? {}, FileModelEncoding.Auto];
2828
let model: Uint8Array | unknown | null = null;
2929
const modelFormat = params.model ?? 'sidecar';
3030
switch (modelFormat) {
3131
case 'sidecar': {
32-
metadata[1] = FileModelEncoding.SidecarBinary;
32+
header[1] = FileModelEncoding.SidecarBinary;
3333
const encoder = this.options.sidecarEncoder;
3434
if (!encoder) throw new Error('NO_SIDECAR_ENCODER');
3535
const [, uint8] = encoder.encode(log.end);
@@ -102,7 +102,7 @@ export class LogEncoder {
102102
default:
103103
throw new Error(`Invalid history format: ${patchFormat}`);
104104
}
105-
return [params.noView ? null : log.end.view(), metadata, model, history];
105+
return [params.noView ? null : log.end.view(), header, model, history];
106106
}
107107

108108
public encode(log: Log, params: EncodingParams): Uint8Array {

src/json-crdt/log/codec/__tests__/LogDecoder.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ describe('can decode from blob', () => {
3636
expect(history!.start().view()).toEqual(undefined);
3737
expect(history!.end.view()).toEqual({foo: 'bar'});
3838
});
39+
40+
test('can store custom metadata keys', () => {
41+
const {log, encoder, decoder} = setup({foo: 'bar'});
42+
const metadata = {
43+
baz: 'qux',
44+
time: 123,
45+
active: true,
46+
};
47+
log.metadata = {...metadata};
48+
const blob = encoder.encode(log, {format: 'seq.cbor'});
49+
const decoded1 = decoder.decode(blob, {format: 'seq.cbor', frontier: true, history: true});
50+
expect(decoded1.frontier?.metadata).toEqual(metadata);
51+
expect(decoded1.history?.metadata).toEqual(metadata);
52+
});
3953
});
4054

4155
const assertEncoding = (log: Log, params: EncodingParams) => {

src/json-crdt/log/codec/types.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import type {FileModelEncoding} from './constants';
22

3-
export type LogMetadata = [
4-
// biome-ignore lint: allow brackets
5-
map: {},
3+
export type LogHeader<metadata extends Record<string, unknown> = Record<string, unknown>> = [
4+
metadata: metadata,
65
modelFormat: FileModelEncoding,
76
];
87

9-
export type LogComponents = [
8+
export type LogComponents<Metadata extends Record<string, unknown> = Record<string, unknown>> = [
109
view: unknown | null,
11-
metadata: LogMetadata,
10+
header: LogHeader<Metadata>,
1211
model: Uint8Array | unknown | null,
1312
history: LogHistory,
1413
];

0 commit comments

Comments
 (0)