@jolly-pixel/asset-server 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/catalog/CatalogExtension.d.ts +37 -0
- package/dist/catalog/CatalogExtension.d.ts.map +1 -0
- package/dist/catalog/CatalogExtension.js +82 -0
- package/dist/catalog/CatalogIdentitySidecar.d.ts +52 -0
- package/dist/catalog/CatalogIdentitySidecar.d.ts.map +1 -0
- package/dist/catalog/CatalogIdentitySidecar.js +132 -0
- package/dist/catalog/CatalogProjection.d.ts +32 -0
- package/dist/catalog/CatalogProjection.d.ts.map +1 -0
- package/dist/catalog/CatalogProjection.js +109 -0
- package/dist/catalog/httpHandler.d.ts +13 -0
- package/dist/catalog/httpHandler.d.ts.map +1 -0
- package/dist/catalog/httpHandler.js +27 -0
- package/dist/catalog/index.d.ts +9 -0
- package/dist/catalog/index.d.ts.map +1 -0
- package/dist/catalog/index.js +4 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/createAssetBackend.d.ts +79 -0
- package/dist/createAssetBackend.d.ts.map +1 -0
- package/dist/createAssetBackend.js +146 -0
- package/dist/events/AssetEvents.d.ts +54 -0
- package/dist/events/AssetEvents.d.ts.map +1 -0
- package/dist/events/AssetEvents.js +102 -0
- package/dist/events/AssetEvents.schema.d.ts +111 -0
- package/dist/events/AssetEvents.schema.d.ts.map +1 -0
- package/dist/events/AssetEvents.schema.js +75 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/kinds/AssetKindHandler.d.ts +38 -0
- package/dist/kinds/AssetKindHandler.d.ts.map +1 -0
- package/dist/kinds/AssetKindHandler.js +1 -0
- package/dist/kinds/AssetKindRegistry.d.ts +14 -0
- package/dist/kinds/AssetKindRegistry.d.ts.map +1 -0
- package/dist/kinds/AssetKindRegistry.js +52 -0
- package/dist/kinds/errors/UnknownAssetKindError.d.ts +5 -0
- package/dist/kinds/errors/UnknownAssetKindError.d.ts.map +1 -0
- package/dist/kinds/errors/UnknownAssetKindError.js +8 -0
- package/dist/kinds/errors/index.d.ts +2 -0
- package/dist/kinds/errors/index.d.ts.map +1 -0
- package/dist/kinds/errors/index.js +1 -0
- package/dist/kinds/handlers/binary.d.ts +10 -0
- package/dist/kinds/handlers/binary.d.ts.map +1 -0
- package/dist/kinds/handlers/binary.js +31 -0
- package/dist/kinds/handlers/index.d.ts +5 -0
- package/dist/kinds/handlers/index.d.ts.map +1 -0
- package/dist/kinds/handlers/index.js +2 -0
- package/dist/kinds/handlers/texture.d.ts +12 -0
- package/dist/kinds/handlers/texture.d.ts.map +1 -0
- package/dist/kinds/handlers/texture.js +27 -0
- package/dist/kinds/index.d.ts +5 -0
- package/dist/kinds/index.d.ts.map +1 -0
- package/dist/kinds/index.js +3 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +9 -0
- package/dist/plugins/vite.d.ts +36 -0
- package/dist/plugins/vite.d.ts.map +1 -0
- package/dist/plugins/vite.js +60 -0
- package/dist/rooms/AssetRoomExtension.d.ts +29 -0
- package/dist/rooms/AssetRoomExtension.d.ts.map +1 -0
- package/dist/rooms/AssetRoomExtension.js +50 -0
- package/dist/rooms/index.d.ts +5 -0
- package/dist/rooms/index.d.ts.map +1 -0
- package/dist/rooms/index.js +2 -0
- package/dist/rooms/registerAssetRooms.d.ts +21 -0
- package/dist/rooms/registerAssetRooms.d.ts.map +1 -0
- package/dist/rooms/registerAssetRooms.js +71 -0
- package/dist/static/contentTypes.d.ts +6 -0
- package/dist/static/contentTypes.d.ts.map +1 -0
- package/dist/static/contentTypes.js +27 -0
- package/dist/static/httpHandler.d.ts +24 -0
- package/dist/static/httpHandler.d.ts.map +1 -0
- package/dist/static/httpHandler.js +105 -0
- package/dist/static/index.d.ts +4 -0
- package/dist/static/index.d.ts.map +1 -0
- package/dist/static/index.js +2 -0
- package/dist/sync/AssetProjector.d.ts +33 -0
- package/dist/sync/AssetProjector.d.ts.map +1 -0
- package/dist/sync/AssetProjector.js +186 -0
- package/dist/sync/AssetStateStore.d.ts +28 -0
- package/dist/sync/AssetStateStore.d.ts.map +1 -0
- package/dist/sync/AssetStateStore.js +86 -0
- package/dist/sync/AssetWriter.d.ts +54 -0
- package/dist/sync/AssetWriter.d.ts.map +1 -0
- package/dist/sync/AssetWriter.js +149 -0
- package/dist/sync/ProjectionState.d.ts +33 -0
- package/dist/sync/ProjectionState.d.ts.map +1 -0
- package/dist/sync/ProjectionState.js +129 -0
- package/dist/sync/Reconciler.d.ts +36 -0
- package/dist/sync/Reconciler.d.ts.map +1 -0
- package/dist/sync/Reconciler.js +182 -0
- package/dist/sync/ReconciliationWatcher.d.ts +28 -0
- package/dist/sync/ReconciliationWatcher.d.ts.map +1 -0
- package/dist/sync/ReconciliationWatcher.js +86 -0
- package/dist/sync/SnapshotScheduler.d.ts +29 -0
- package/dist/sync/SnapshotScheduler.d.ts.map +1 -0
- package/dist/sync/SnapshotScheduler.js +136 -0
- package/dist/sync/foldProjection.d.ts +12 -0
- package/dist/sync/foldProjection.d.ts.map +1 -0
- package/dist/sync/foldProjection.js +32 -0
- package/dist/sync/index.d.ts +19 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/matchRenames.d.ts +45 -0
- package/dist/sync/matchRenames.d.ts.map +1 -0
- package/dist/sync/matchRenames.js +85 -0
- package/dist/utils/TaskChain.d.ts +9 -0
- package/dist/utils/TaskChain.d.ts.map +1 -0
- package/dist/utils/TaskChain.js +14 -0
- package/dist/utils/contentHash.d.ts +2 -0
- package/dist/utils/contentHash.d.ts.map +1 -0
- package/dist/utils/contentHash.js +7 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/workspace/createAssetWorkspace.d.ts +71 -0
- package/dist/workspace/createAssetWorkspace.d.ts.map +1 -0
- package/dist/workspace/createAssetWorkspace.js +72 -0
- package/dist/workspace/index.d.ts +5 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +2 -0
- package/dist/workspace/seedAssetSource.d.ts +5 -0
- package/dist/workspace/seedAssetSource.d.ts.map +1 -0
- package/dist/workspace/seedAssetSource.js +20 -0
- package/docs/AssetBackend.md +73 -0
- package/docs/AssetKinds.md +204 -0
- package/docs/AssetWriter.md +62 -0
- package/docs/Catalog.md +71 -0
- package/docs/Rooms.md +70 -0
- package/docs/Sync.md +146 -0
- package/docs/Workspace.md +172 -0
- package/package.json +83 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type AssetSource } from "@jolly-pixel/asset-source";
|
|
2
|
+
import { type Logger } from "../logger.ts";
|
|
3
|
+
export interface ProjectionFailure {
|
|
4
|
+
readonly eventId: number;
|
|
5
|
+
readonly attempts: number;
|
|
6
|
+
readonly reason: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProjectionStateParse {
|
|
9
|
+
readonly data: ProjectionStateData | null;
|
|
10
|
+
readonly dropped: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ProjectionStateData {
|
|
13
|
+
readonly version: 1;
|
|
14
|
+
readonly checkpoints: Record<string, number>;
|
|
15
|
+
readonly failures: Record<string, ProjectionFailure>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Persists each asset's last successfully projected event id.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ProjectionState {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(source: AssetSource, data?: ProjectionStateData | null);
|
|
23
|
+
checkpoint(assetId: string): number;
|
|
24
|
+
failure(assetId: string): ProjectionFailure | undefined;
|
|
25
|
+
advance(assetId: string, eventId: number): void;
|
|
26
|
+
recordFailure(assetId: string, eventId: number, reason: string): void;
|
|
27
|
+
forget(assetId: string): void;
|
|
28
|
+
toJSON(): ProjectionStateData;
|
|
29
|
+
save(): Promise<void>;
|
|
30
|
+
static load(source: AssetSource, logger?: Logger): Promise<ProjectionState>;
|
|
31
|
+
static parse(input: unknown): ProjectionStateParse;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=ProjectionState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectionState.d.ts","sourceRoot":"","sources":["../../src/sync/ProjectionState.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAEL,KAAK,MAAM,EACZ,MAAM,cAAc,CAAC;AA+BtB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAOD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CACtD;AAED;;GAEG;AACH,qBAAa,eAAe;;gBAMxB,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE,mBAAmB,GAAG,IAAW;IAqBzC,UAAU,CACR,OAAO,EAAE,MAAM,GACd,MAAM;IAIT,OAAO,CACL,OAAO,EAAE,MAAM,GACd,iBAAiB,GAAG,SAAS;IAIhC,OAAO,CACL,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,IAAI;IAKP,aAAa,CACX,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,IAAI;IASP,MAAM,CACJ,OAAO,EAAE,MAAM,GACd,IAAI;IAKP,MAAM,IAAI,mBAAmB;IAgB7B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;WAQR,IAAI,CACf,MAAM,EAAE,WAAW,EACnB,MAAM,GAAE,MAAuB,GAC9B,OAAO,CAAC,eAAe,CAAC;IAmB3B,MAAM,CAAC,KAAK,CACV,KAAK,EAAE,OAAO,GACb,oBAAoB;CA0BxB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Import Third-party Dependencies
|
|
2
|
+
import { defineSchema, Validator } from "ata-validator";
|
|
3
|
+
import { readJsonFile, writeJsonFile } from "@jolly-pixel/asset-source";
|
|
4
|
+
// Import Internal Dependencies
|
|
5
|
+
import { silentLogger } from "../logger.js";
|
|
6
|
+
import { PROJECTION_STATE_PATH } from "../constants.js";
|
|
7
|
+
// CONSTANTS
|
|
8
|
+
const kStateVersion = 1;
|
|
9
|
+
const kValidatorOptions = { useDefaults: false };
|
|
10
|
+
const kRecordValidator = new Validator(defineSchema({ type: "object" }), kValidatorOptions);
|
|
11
|
+
const kCheckpointValidator = new Validator(defineSchema({ type: "integer" }), kValidatorOptions);
|
|
12
|
+
const kFailureValidator = new Validator(defineSchema({
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
eventId: { type: "integer" },
|
|
16
|
+
attempts: { type: "integer" },
|
|
17
|
+
reason: { type: "string" }
|
|
18
|
+
},
|
|
19
|
+
required: [
|
|
20
|
+
"eventId",
|
|
21
|
+
"attempts",
|
|
22
|
+
"reason"
|
|
23
|
+
]
|
|
24
|
+
}), kValidatorOptions);
|
|
25
|
+
/**
|
|
26
|
+
* Persists each asset's last successfully projected event id.
|
|
27
|
+
*/
|
|
28
|
+
export class ProjectionState {
|
|
29
|
+
#checkpoints = new Map();
|
|
30
|
+
#failures = new Map();
|
|
31
|
+
#source;
|
|
32
|
+
constructor(source, data = null) {
|
|
33
|
+
this.#source = source;
|
|
34
|
+
if (data === null) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const [assetId, eventId] of Object.entries(data.checkpoints)) {
|
|
38
|
+
this.#checkpoints.set(assetId, eventId);
|
|
39
|
+
}
|
|
40
|
+
for (const [assetId, failure] of Object.entries(data.failures)) {
|
|
41
|
+
this.#failures.set(assetId, failure);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
checkpoint(assetId) {
|
|
45
|
+
return this.#checkpoints.get(assetId) ?? 0;
|
|
46
|
+
}
|
|
47
|
+
failure(assetId) {
|
|
48
|
+
return this.#failures.get(assetId);
|
|
49
|
+
}
|
|
50
|
+
advance(assetId, eventId) {
|
|
51
|
+
this.#checkpoints.set(assetId, eventId);
|
|
52
|
+
this.#failures.delete(assetId);
|
|
53
|
+
}
|
|
54
|
+
recordFailure(assetId, eventId, reason) {
|
|
55
|
+
const previous = this.#failures.get(assetId);
|
|
56
|
+
this.#failures.set(assetId, {
|
|
57
|
+
eventId,
|
|
58
|
+
attempts: previous?.eventId === eventId ? previous.attempts + 1 : 1,
|
|
59
|
+
reason
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
forget(assetId) {
|
|
63
|
+
this.#checkpoints.delete(assetId);
|
|
64
|
+
this.#failures.delete(assetId);
|
|
65
|
+
}
|
|
66
|
+
toJSON() {
|
|
67
|
+
return {
|
|
68
|
+
version: kStateVersion,
|
|
69
|
+
checkpoints: Object.fromEntries([...this.#checkpoints.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
70
|
+
failures: Object.fromEntries([...this.#failures.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
save() {
|
|
74
|
+
return writeJsonFile(this.#source, PROJECTION_STATE_PATH, this.toJSON());
|
|
75
|
+
}
|
|
76
|
+
static async load(source, logger = silentLogger()) {
|
|
77
|
+
const { data, dropped } = ProjectionState.parse(await readJsonFile(source, PROJECTION_STATE_PATH));
|
|
78
|
+
if (dropped > 0) {
|
|
79
|
+
logger
|
|
80
|
+
.withMetadata({
|
|
81
|
+
path: PROJECTION_STATE_PATH,
|
|
82
|
+
dropped
|
|
83
|
+
})
|
|
84
|
+
.warn("projection state entries dropped");
|
|
85
|
+
}
|
|
86
|
+
return new ProjectionState(source, data);
|
|
87
|
+
}
|
|
88
|
+
static parse(input) {
|
|
89
|
+
if (!kRecordValidator.isValidObject(input)) {
|
|
90
|
+
return {
|
|
91
|
+
data: null,
|
|
92
|
+
dropped: 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const checkpoints = pickEntries(input.checkpoints, kCheckpointValidator);
|
|
96
|
+
const failures = pickEntries(input.failures, kFailureValidator);
|
|
97
|
+
return {
|
|
98
|
+
data: {
|
|
99
|
+
version: kStateVersion,
|
|
100
|
+
checkpoints: checkpoints.entries,
|
|
101
|
+
failures: failures.entries
|
|
102
|
+
},
|
|
103
|
+
dropped: checkpoints.dropped + failures.dropped
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function pickEntries(input, validator) {
|
|
108
|
+
if (!kRecordValidator.isValidObject(input)) {
|
|
109
|
+
return {
|
|
110
|
+
entries: {},
|
|
111
|
+
dropped: 0
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const entries = {};
|
|
115
|
+
let dropped = 0;
|
|
116
|
+
for (const [key, value] of Object.entries(input)) {
|
|
117
|
+
const result = validator.validate(value);
|
|
118
|
+
if (result.valid) {
|
|
119
|
+
entries[key] = result.data;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
dropped += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
entries,
|
|
127
|
+
dropped
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type * as EventStore from "@jolly-pixel/event-store";
|
|
2
|
+
import { type Result } from "@openally/result";
|
|
3
|
+
import type { AssetSource } from "@jolly-pixel/asset-source";
|
|
4
|
+
import type { AssetKindRegistry } from "../kinds/AssetKindRegistry.ts";
|
|
5
|
+
import { type Logger } from "../logger.ts";
|
|
6
|
+
import type { AssetProjector } from "./AssetProjector.ts";
|
|
7
|
+
import type { AssetWriter } from "./AssetWriter.ts";
|
|
8
|
+
export interface ReconcileReport {
|
|
9
|
+
readonly created: number;
|
|
10
|
+
readonly updated: number;
|
|
11
|
+
readonly renamed: number;
|
|
12
|
+
readonly deleted: number;
|
|
13
|
+
/**
|
|
14
|
+
* Entries skipped after an append or read failure.
|
|
15
|
+
*/
|
|
16
|
+
readonly failed: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ReconcilerOptions {
|
|
19
|
+
source: AssetSource;
|
|
20
|
+
projector: AssetProjector;
|
|
21
|
+
writer: AssetWriter;
|
|
22
|
+
kinds: AssetKindRegistry;
|
|
23
|
+
actor?: EventStore.Actor;
|
|
24
|
+
logger?: Logger;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reconciles physical files against the last projected event state.
|
|
28
|
+
*
|
|
29
|
+
* Hash equality suppresses echoed writes and makes notifications idempotent.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Reconciler {
|
|
32
|
+
#private;
|
|
33
|
+
constructor(options: ReconcilerOptions);
|
|
34
|
+
reconcile(): Promise<Result<ReconcileReport, Error>>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=Reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Reconciler.d.ts","sourceRoot":"","sources":["../../src/sync/Reconciler.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,UAAU,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAG7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAQvE,OAAO,EAEL,KAAK,MAAM,EACZ,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AASpD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AA+BD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,iBAAiB,CAAC;IACzB,KAAK,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,qBAAa,UAAU;;gBASnB,OAAO,EAAE,iBAAiB;IAUtB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;CA4K3D"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { Err, Ok } from "@openally/result";
|
|
4
|
+
import { contentHash } from "../utils/contentHash.js";
|
|
5
|
+
import { matchRenames } from "./matchRenames.js";
|
|
6
|
+
import { silentLogger } from "../logger.js";
|
|
7
|
+
// CONSTANTS
|
|
8
|
+
const kWatcherActor = {
|
|
9
|
+
type: "system",
|
|
10
|
+
source: "fs-watcher"
|
|
11
|
+
};
|
|
12
|
+
const kReadConcurrency = 16;
|
|
13
|
+
/**
|
|
14
|
+
* Reconciles physical files against the last projected event state.
|
|
15
|
+
*
|
|
16
|
+
* Hash equality suppresses echoed writes and makes notifications idempotent.
|
|
17
|
+
*/
|
|
18
|
+
export class Reconciler {
|
|
19
|
+
#source;
|
|
20
|
+
#projector;
|
|
21
|
+
#writer;
|
|
22
|
+
#kinds;
|
|
23
|
+
#actor;
|
|
24
|
+
#logger;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.#source = options.source;
|
|
27
|
+
this.#projector = options.projector;
|
|
28
|
+
this.#writer = options.writer;
|
|
29
|
+
this.#kinds = options.kinds;
|
|
30
|
+
this.#actor = options.actor ?? kWatcherActor;
|
|
31
|
+
this.#logger = options.logger ?? silentLogger();
|
|
32
|
+
}
|
|
33
|
+
async reconcile() {
|
|
34
|
+
let scan;
|
|
35
|
+
try {
|
|
36
|
+
scan = await this.#observe();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return Err(asError(error));
|
|
40
|
+
}
|
|
41
|
+
const projected = [];
|
|
42
|
+
for (const { assetId, projection } of this.#projector.projections()) {
|
|
43
|
+
if (scan.unreadable.has(projection.path)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
projected.push({
|
|
47
|
+
id: assetId,
|
|
48
|
+
path: projection.path,
|
|
49
|
+
kind: projection.kind,
|
|
50
|
+
hash: projection.hash
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const diff = matchRenames(projected, scan.entries);
|
|
54
|
+
const report = {
|
|
55
|
+
created: 0,
|
|
56
|
+
updated: 0,
|
|
57
|
+
renamed: 0,
|
|
58
|
+
deleted: 0,
|
|
59
|
+
failed: scan.unreadable.size
|
|
60
|
+
};
|
|
61
|
+
for (const operation of this.#plan(diff)) {
|
|
62
|
+
await this.#apply(operation, report);
|
|
63
|
+
}
|
|
64
|
+
if (this.#total(report) > 0) {
|
|
65
|
+
this.#logger
|
|
66
|
+
.withMetadata({ ...report })
|
|
67
|
+
.info("reconciled external changes");
|
|
68
|
+
}
|
|
69
|
+
return Ok(report);
|
|
70
|
+
}
|
|
71
|
+
*#plan(diff) {
|
|
72
|
+
for (const entry of diff.deleted) {
|
|
73
|
+
yield {
|
|
74
|
+
outcome: "deleted",
|
|
75
|
+
path: entry.path,
|
|
76
|
+
run: () => this.#writer.remove({
|
|
77
|
+
assetId: entry.id,
|
|
78
|
+
actor: this.#actor,
|
|
79
|
+
alreadyProjected: true
|
|
80
|
+
})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
for (const entry of diff.renamed) {
|
|
84
|
+
yield {
|
|
85
|
+
outcome: "renamed",
|
|
86
|
+
path: entry.to,
|
|
87
|
+
run: () => this.#writer.rename({
|
|
88
|
+
assetId: entry.id,
|
|
89
|
+
to: entry.to,
|
|
90
|
+
actor: this.#actor,
|
|
91
|
+
alreadyProjected: true
|
|
92
|
+
})
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
for (const entry of diff.updated) {
|
|
96
|
+
yield {
|
|
97
|
+
outcome: "updated",
|
|
98
|
+
path: entry.path,
|
|
99
|
+
run: async () => this.#writer.update({
|
|
100
|
+
assetId: entry.id,
|
|
101
|
+
data: await this.#source.read(entry.path),
|
|
102
|
+
actor: this.#actor,
|
|
103
|
+
alreadyProjected: true
|
|
104
|
+
})
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
for (const entry of diff.created) {
|
|
108
|
+
yield {
|
|
109
|
+
outcome: "created",
|
|
110
|
+
path: entry.path,
|
|
111
|
+
run: async () => this.#writer.create({
|
|
112
|
+
path: entry.path,
|
|
113
|
+
data: await this.#source.read(entry.path),
|
|
114
|
+
kind: this.#kinds.resolve(entry.path).kind,
|
|
115
|
+
assetId: this.#writer.identity.byPath(entry.path)?.id ?? randomUUID(),
|
|
116
|
+
actor: this.#actor,
|
|
117
|
+
alreadyProjected: true
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async #apply(operation, report) {
|
|
123
|
+
const result = await operation.run()
|
|
124
|
+
.catch((error) => Err(asError(error)));
|
|
125
|
+
if (result.ok) {
|
|
126
|
+
report[operation.outcome] += 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
report.failed += 1;
|
|
130
|
+
this.#logger
|
|
131
|
+
.withMetadata({
|
|
132
|
+
path: operation.path,
|
|
133
|
+
outcome: operation.outcome,
|
|
134
|
+
reason: result.val.message
|
|
135
|
+
})
|
|
136
|
+
.error("asset reconciliation failed");
|
|
137
|
+
}
|
|
138
|
+
#total(report) {
|
|
139
|
+
return report.created +
|
|
140
|
+
report.updated +
|
|
141
|
+
report.renamed +
|
|
142
|
+
report.deleted +
|
|
143
|
+
report.failed;
|
|
144
|
+
}
|
|
145
|
+
async #observe() {
|
|
146
|
+
const paths = await this.#source.list();
|
|
147
|
+
const entries = [];
|
|
148
|
+
const unreadable = new Set();
|
|
149
|
+
for (let index = 0; index < paths.length; index += kReadConcurrency) {
|
|
150
|
+
const batch = paths.slice(index, index + kReadConcurrency);
|
|
151
|
+
const results = await Promise.all(batch.map(async (path) => {
|
|
152
|
+
try {
|
|
153
|
+
return {
|
|
154
|
+
path,
|
|
155
|
+
entry: {
|
|
156
|
+
path,
|
|
157
|
+
hash: contentHash(await this.#source.read(path))
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.#logger
|
|
163
|
+
.withMetadata({ path, reason: asError(error).message })
|
|
164
|
+
.warn("asset not readable, left untouched");
|
|
165
|
+
return { path, entry: null };
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
for (const { path, entry } of results) {
|
|
169
|
+
if (entry === null) {
|
|
170
|
+
unreadable.add(path);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
entries.push(entry);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { entries, unreadable };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function asError(error) {
|
|
181
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
182
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AssetSource } from "@jolly-pixel/asset-source";
|
|
2
|
+
import { type Logger } from "../logger.ts";
|
|
3
|
+
import type { Reconciler } from "./Reconciler.ts";
|
|
4
|
+
export interface ReconciliationWatcherOptions {
|
|
5
|
+
source: AssetSource;
|
|
6
|
+
reconciler: Reconciler;
|
|
7
|
+
/**
|
|
8
|
+
* Quiet period, in milliseconds, before a batch of notifications turns
|
|
9
|
+
* into one reconciliation pass.
|
|
10
|
+
* @default 200
|
|
11
|
+
*/
|
|
12
|
+
debounce?: number;
|
|
13
|
+
logger?: Logger;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Coalesces filesystem notifications into idempotent reconciliation passes.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ReconciliationWatcher {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(options: ReconciliationWatcherOptions);
|
|
21
|
+
get watching(): boolean;
|
|
22
|
+
start(): void;
|
|
23
|
+
notify(path: string): void;
|
|
24
|
+
run(): Promise<void>;
|
|
25
|
+
settle(): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=ReconciliationWatcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReconciliationWatcher.d.ts","sourceRoot":"","sources":["../../src/sync/ReconciliationWatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAG7D,OAAO,EAEL,KAAK,MAAM,EACZ,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKlD,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,qBAAqB;;gBAY9B,OAAO,EAAE,4BAA4B;IAQvC,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,KAAK,IAAI,IAAI;IAWb,MAAM,CACJ,IAAI,EAAE,MAAM,GACX,IAAI;IAeP,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBd,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA6B7B"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Import Internal Dependencies
|
|
2
|
+
import { silentLogger } from "../logger.js";
|
|
3
|
+
// CONSTANTS
|
|
4
|
+
const kDefaultDebounce = 200;
|
|
5
|
+
/**
|
|
6
|
+
* Coalesces filesystem notifications into idempotent reconciliation passes.
|
|
7
|
+
*/
|
|
8
|
+
export class ReconciliationWatcher {
|
|
9
|
+
#source;
|
|
10
|
+
#reconciler;
|
|
11
|
+
#debounce;
|
|
12
|
+
#logger;
|
|
13
|
+
#unwatch = null;
|
|
14
|
+
#handle = null;
|
|
15
|
+
#running = null;
|
|
16
|
+
#again = false;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.#source = options.source;
|
|
19
|
+
this.#reconciler = options.reconciler;
|
|
20
|
+
this.#debounce = options.debounce ?? kDefaultDebounce;
|
|
21
|
+
this.#logger = options.logger ?? silentLogger();
|
|
22
|
+
}
|
|
23
|
+
get watching() {
|
|
24
|
+
return this.#unwatch !== null;
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this.#unwatch !== null ||
|
|
28
|
+
this.#source.watch === undefined) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.#unwatch = this.#source.watch((path) => this.notify(path));
|
|
32
|
+
}
|
|
33
|
+
notify(path) {
|
|
34
|
+
this.#logger
|
|
35
|
+
.withMetadata({ path })
|
|
36
|
+
.debug("filesystem change observed");
|
|
37
|
+
if (this.#handle !== null) {
|
|
38
|
+
clearTimeout(this.#handle);
|
|
39
|
+
}
|
|
40
|
+
this.#handle = setTimeout(() => void this.run(), this.#debounce);
|
|
41
|
+
this.#handle.unref();
|
|
42
|
+
}
|
|
43
|
+
run() {
|
|
44
|
+
if (this.#handle !== null) {
|
|
45
|
+
clearTimeout(this.#handle);
|
|
46
|
+
this.#handle = null;
|
|
47
|
+
}
|
|
48
|
+
if (this.#running !== null) {
|
|
49
|
+
this.#again = true;
|
|
50
|
+
return this.#running;
|
|
51
|
+
}
|
|
52
|
+
const pass = this.#run();
|
|
53
|
+
this.#running = pass;
|
|
54
|
+
return pass;
|
|
55
|
+
}
|
|
56
|
+
async settle() {
|
|
57
|
+
await this.#running;
|
|
58
|
+
}
|
|
59
|
+
async close() {
|
|
60
|
+
if (this.#unwatch !== null) {
|
|
61
|
+
this.#unwatch();
|
|
62
|
+
this.#unwatch = null;
|
|
63
|
+
}
|
|
64
|
+
if (this.#handle !== null) {
|
|
65
|
+
clearTimeout(this.#handle);
|
|
66
|
+
this.#handle = null;
|
|
67
|
+
}
|
|
68
|
+
await this.#running;
|
|
69
|
+
}
|
|
70
|
+
async #run() {
|
|
71
|
+
try {
|
|
72
|
+
do {
|
|
73
|
+
this.#again = false;
|
|
74
|
+
const result = await this.#reconciler.reconcile();
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
this.#logger
|
|
77
|
+
.withMetadata({ reason: result.val.message })
|
|
78
|
+
.error("reconciliation failed");
|
|
79
|
+
}
|
|
80
|
+
} while (this.#again);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
this.#running = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type * as EventStore from "@jolly-pixel/event-store";
|
|
2
|
+
import { type Logger } from "../logger.ts";
|
|
3
|
+
import { type AssetEventDataMap } from "../events/AssetEvents.ts";
|
|
4
|
+
import type { SnapshotPolicy } from "../kinds/AssetKindHandler.ts";
|
|
5
|
+
import type { AssetStateStore } from "./AssetStateStore.ts";
|
|
6
|
+
import type { AssetProjector } from "./AssetProjector.ts";
|
|
7
|
+
export interface SnapshotSchedulerOptions {
|
|
8
|
+
eventStore: EventStore.TypedEventStore<AssetEventDataMap>;
|
|
9
|
+
states: AssetStateStore;
|
|
10
|
+
projector: AssetProjector;
|
|
11
|
+
snapshot?: SnapshotPolicy;
|
|
12
|
+
logger?: Logger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Schedules snapshots by quiet period and maximum delay per asset.
|
|
16
|
+
*
|
|
17
|
+
* Snapshots append events; the projector performs the physical write.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SnapshotScheduler {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(options: SnapshotSchedulerOptions);
|
|
22
|
+
get pending(): number;
|
|
23
|
+
start(): void;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
schedule(assetId: string): void;
|
|
26
|
+
flush(assetId?: string): Promise<void>;
|
|
27
|
+
snapshot(assetId: string): Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=SnapshotScheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SnapshotScheduler.d.ts","sourceRoot":"","sources":["../../src/sync/SnapshotScheduler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,UAAU,MAAM,0BAA0B,CAAC;AAG5D,OAAO,EAEL,KAAK,MAAM,EACZ,MAAM,cAAc,CAAC;AAEtB,OAAO,EAIL,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAY1D,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,UAAU,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC;IAC1D,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,qBAAa,iBAAiB;;gBAY1B,OAAO,EAAE,wBAAwB;IAYnC,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,KAAK,IAAI,IAAI;IAQP,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,QAAQ,CACN,OAAO,EAAE,MAAM,GACd,IAAI;IAmCD,KAAK,CACT,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC;IAiBhB,QAAQ,CACN,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC;CA+DpB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Import Internal Dependencies
|
|
2
|
+
import { silentLogger } from "../logger.js";
|
|
3
|
+
import { contentHash } from "../utils/contentHash.js";
|
|
4
|
+
import { ASSET_UPDATED, encodeContent, isAssetEventType } from "../events/AssetEvents.js";
|
|
5
|
+
import { TaskChain } from "../utils/TaskChain.js";
|
|
6
|
+
// CONSTANTS
|
|
7
|
+
const kDefaultDelay = 2_000;
|
|
8
|
+
const kDefaultMaxDelay = 30_000;
|
|
9
|
+
/**
|
|
10
|
+
* Schedules snapshots by quiet period and maximum delay per asset.
|
|
11
|
+
*
|
|
12
|
+
* Snapshots append events; the projector performs the physical write.
|
|
13
|
+
*/
|
|
14
|
+
export class SnapshotScheduler {
|
|
15
|
+
#eventStore;
|
|
16
|
+
#states;
|
|
17
|
+
#projector;
|
|
18
|
+
#policy;
|
|
19
|
+
#logger;
|
|
20
|
+
#pending = new Map();
|
|
21
|
+
#chains = new Map();
|
|
22
|
+
#unsubscribe = null;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#eventStore = options.eventStore;
|
|
25
|
+
this.#states = options.states;
|
|
26
|
+
this.#projector = options.projector;
|
|
27
|
+
this.#policy = {
|
|
28
|
+
delay: options.snapshot?.delay ?? kDefaultDelay,
|
|
29
|
+
maxDelay: options.snapshot?.maxDelay ?? kDefaultMaxDelay
|
|
30
|
+
};
|
|
31
|
+
this.#logger = options.logger ?? silentLogger();
|
|
32
|
+
}
|
|
33
|
+
get pending() {
|
|
34
|
+
return this.#pending.size;
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
this.#unsubscribe ??= this.#eventStore.subscribe((event) => {
|
|
38
|
+
if (!isAssetEventType(event.eventType)) {
|
|
39
|
+
this.schedule(event.assetId);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async close() {
|
|
44
|
+
this.#unsubscribe?.();
|
|
45
|
+
this.#unsubscribe = null;
|
|
46
|
+
await this.flush();
|
|
47
|
+
}
|
|
48
|
+
schedule(assetId) {
|
|
49
|
+
const entry = this.#states.get(assetId);
|
|
50
|
+
if (entry === undefined) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const policy = {
|
|
54
|
+
delay: entry.handler.snapshot?.delay ?? this.#policy.delay,
|
|
55
|
+
maxDelay: entry.handler.snapshot?.maxDelay ?? this.#policy.maxDelay
|
|
56
|
+
};
|
|
57
|
+
const pending = this.#pending.get(assetId);
|
|
58
|
+
const firstEventAt = pending?.firstEventAt ?? Date.now();
|
|
59
|
+
if (pending !== undefined) {
|
|
60
|
+
clearTimeout(pending.handle);
|
|
61
|
+
}
|
|
62
|
+
const elapsed = Date.now() - firstEventAt;
|
|
63
|
+
const delay = Math.max(0, Math.min(policy.delay, policy.maxDelay - elapsed));
|
|
64
|
+
const handle = setTimeout(() => void this.snapshot(assetId), delay);
|
|
65
|
+
handle.unref();
|
|
66
|
+
this.#pending.set(assetId, {
|
|
67
|
+
firstEventAt,
|
|
68
|
+
handle
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async flush(assetId) {
|
|
72
|
+
const targets = assetId === undefined ?
|
|
73
|
+
[...this.#pending.keys()] :
|
|
74
|
+
[assetId].filter((id) => this.#pending.has(id));
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
await this.snapshot(target);
|
|
77
|
+
}
|
|
78
|
+
const chains = assetId === undefined ?
|
|
79
|
+
[...this.#chains.values()] :
|
|
80
|
+
[this.#chains.get(assetId)];
|
|
81
|
+
await Promise.all(chains.map((chain) => chain?.settled()));
|
|
82
|
+
}
|
|
83
|
+
snapshot(assetId) {
|
|
84
|
+
let chain = this.#chains.get(assetId);
|
|
85
|
+
if (chain === undefined) {
|
|
86
|
+
chain = new TaskChain();
|
|
87
|
+
this.#chains.set(assetId, chain);
|
|
88
|
+
}
|
|
89
|
+
return chain.run(() => this.#snapshot(assetId));
|
|
90
|
+
}
|
|
91
|
+
async #snapshot(assetId) {
|
|
92
|
+
const pending = this.#pending.get(assetId);
|
|
93
|
+
if (pending !== undefined) {
|
|
94
|
+
clearTimeout(pending.handle);
|
|
95
|
+
this.#pending.delete(assetId);
|
|
96
|
+
}
|
|
97
|
+
const entry = this.#states.get(assetId);
|
|
98
|
+
const desired = this.#projector.desired(assetId);
|
|
99
|
+
if (entry === undefined || desired === null) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const data = await entry.handler.serialize(entry.state);
|
|
103
|
+
const hash = contentHash(data);
|
|
104
|
+
if (hash === desired.hash) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const eventData = {
|
|
108
|
+
path: desired.path,
|
|
109
|
+
kind: desired.kind,
|
|
110
|
+
hash,
|
|
111
|
+
size: data.byteLength,
|
|
112
|
+
content: encodeContent(data)
|
|
113
|
+
};
|
|
114
|
+
const appended = this.#eventStore.writer.append({
|
|
115
|
+
assetType: entry.kind,
|
|
116
|
+
assetId,
|
|
117
|
+
eventType: ASSET_UPDATED,
|
|
118
|
+
eventData,
|
|
119
|
+
actor: {
|
|
120
|
+
type: "system",
|
|
121
|
+
source: "snapshot"
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
if (!appended.ok) {
|
|
125
|
+
this.#logger
|
|
126
|
+
.withMetadata({
|
|
127
|
+
assetId,
|
|
128
|
+
reason: appended.val.message
|
|
129
|
+
})
|
|
130
|
+
.error("asset snapshot failed");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await this.#projector.flush(assetId);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|