@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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/catalog/CatalogExtension.d.ts +37 -0
  4. package/dist/catalog/CatalogExtension.d.ts.map +1 -0
  5. package/dist/catalog/CatalogExtension.js +82 -0
  6. package/dist/catalog/CatalogIdentitySidecar.d.ts +52 -0
  7. package/dist/catalog/CatalogIdentitySidecar.d.ts.map +1 -0
  8. package/dist/catalog/CatalogIdentitySidecar.js +132 -0
  9. package/dist/catalog/CatalogProjection.d.ts +32 -0
  10. package/dist/catalog/CatalogProjection.d.ts.map +1 -0
  11. package/dist/catalog/CatalogProjection.js +109 -0
  12. package/dist/catalog/httpHandler.d.ts +13 -0
  13. package/dist/catalog/httpHandler.d.ts.map +1 -0
  14. package/dist/catalog/httpHandler.js +27 -0
  15. package/dist/catalog/index.d.ts +9 -0
  16. package/dist/catalog/index.d.ts.map +1 -0
  17. package/dist/catalog/index.js +4 -0
  18. package/dist/constants.d.ts +9 -0
  19. package/dist/constants.d.ts.map +1 -0
  20. package/dist/constants.js +13 -0
  21. package/dist/createAssetBackend.d.ts +79 -0
  22. package/dist/createAssetBackend.d.ts.map +1 -0
  23. package/dist/createAssetBackend.js +146 -0
  24. package/dist/events/AssetEvents.d.ts +54 -0
  25. package/dist/events/AssetEvents.d.ts.map +1 -0
  26. package/dist/events/AssetEvents.js +102 -0
  27. package/dist/events/AssetEvents.schema.d.ts +111 -0
  28. package/dist/events/AssetEvents.schema.d.ts.map +1 -0
  29. package/dist/events/AssetEvents.schema.js +75 -0
  30. package/dist/events/index.d.ts +3 -0
  31. package/dist/events/index.d.ts.map +1 -0
  32. package/dist/events/index.js +1 -0
  33. package/dist/index.d.ts +13 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +11 -0
  36. package/dist/kinds/AssetKindHandler.d.ts +38 -0
  37. package/dist/kinds/AssetKindHandler.d.ts.map +1 -0
  38. package/dist/kinds/AssetKindHandler.js +1 -0
  39. package/dist/kinds/AssetKindRegistry.d.ts +14 -0
  40. package/dist/kinds/AssetKindRegistry.d.ts.map +1 -0
  41. package/dist/kinds/AssetKindRegistry.js +52 -0
  42. package/dist/kinds/errors/UnknownAssetKindError.d.ts +5 -0
  43. package/dist/kinds/errors/UnknownAssetKindError.d.ts.map +1 -0
  44. package/dist/kinds/errors/UnknownAssetKindError.js +8 -0
  45. package/dist/kinds/errors/index.d.ts +2 -0
  46. package/dist/kinds/errors/index.d.ts.map +1 -0
  47. package/dist/kinds/errors/index.js +1 -0
  48. package/dist/kinds/handlers/binary.d.ts +10 -0
  49. package/dist/kinds/handlers/binary.d.ts.map +1 -0
  50. package/dist/kinds/handlers/binary.js +31 -0
  51. package/dist/kinds/handlers/index.d.ts +5 -0
  52. package/dist/kinds/handlers/index.d.ts.map +1 -0
  53. package/dist/kinds/handlers/index.js +2 -0
  54. package/dist/kinds/handlers/texture.d.ts +12 -0
  55. package/dist/kinds/handlers/texture.d.ts.map +1 -0
  56. package/dist/kinds/handlers/texture.js +27 -0
  57. package/dist/kinds/index.d.ts +5 -0
  58. package/dist/kinds/index.d.ts.map +1 -0
  59. package/dist/kinds/index.js +3 -0
  60. package/dist/logger.d.ts +4 -0
  61. package/dist/logger.d.ts.map +1 -0
  62. package/dist/logger.js +9 -0
  63. package/dist/plugins/vite.d.ts +36 -0
  64. package/dist/plugins/vite.d.ts.map +1 -0
  65. package/dist/plugins/vite.js +60 -0
  66. package/dist/rooms/AssetRoomExtension.d.ts +29 -0
  67. package/dist/rooms/AssetRoomExtension.d.ts.map +1 -0
  68. package/dist/rooms/AssetRoomExtension.js +50 -0
  69. package/dist/rooms/index.d.ts +5 -0
  70. package/dist/rooms/index.d.ts.map +1 -0
  71. package/dist/rooms/index.js +2 -0
  72. package/dist/rooms/registerAssetRooms.d.ts +21 -0
  73. package/dist/rooms/registerAssetRooms.d.ts.map +1 -0
  74. package/dist/rooms/registerAssetRooms.js +71 -0
  75. package/dist/static/contentTypes.d.ts +6 -0
  76. package/dist/static/contentTypes.d.ts.map +1 -0
  77. package/dist/static/contentTypes.js +27 -0
  78. package/dist/static/httpHandler.d.ts +24 -0
  79. package/dist/static/httpHandler.d.ts.map +1 -0
  80. package/dist/static/httpHandler.js +105 -0
  81. package/dist/static/index.d.ts +4 -0
  82. package/dist/static/index.d.ts.map +1 -0
  83. package/dist/static/index.js +2 -0
  84. package/dist/sync/AssetProjector.d.ts +33 -0
  85. package/dist/sync/AssetProjector.d.ts.map +1 -0
  86. package/dist/sync/AssetProjector.js +186 -0
  87. package/dist/sync/AssetStateStore.d.ts +28 -0
  88. package/dist/sync/AssetStateStore.d.ts.map +1 -0
  89. package/dist/sync/AssetStateStore.js +86 -0
  90. package/dist/sync/AssetWriter.d.ts +54 -0
  91. package/dist/sync/AssetWriter.d.ts.map +1 -0
  92. package/dist/sync/AssetWriter.js +149 -0
  93. package/dist/sync/ProjectionState.d.ts +33 -0
  94. package/dist/sync/ProjectionState.d.ts.map +1 -0
  95. package/dist/sync/ProjectionState.js +129 -0
  96. package/dist/sync/Reconciler.d.ts +36 -0
  97. package/dist/sync/Reconciler.d.ts.map +1 -0
  98. package/dist/sync/Reconciler.js +182 -0
  99. package/dist/sync/ReconciliationWatcher.d.ts +28 -0
  100. package/dist/sync/ReconciliationWatcher.d.ts.map +1 -0
  101. package/dist/sync/ReconciliationWatcher.js +86 -0
  102. package/dist/sync/SnapshotScheduler.d.ts +29 -0
  103. package/dist/sync/SnapshotScheduler.d.ts.map +1 -0
  104. package/dist/sync/SnapshotScheduler.js +136 -0
  105. package/dist/sync/foldProjection.d.ts +12 -0
  106. package/dist/sync/foldProjection.d.ts.map +1 -0
  107. package/dist/sync/foldProjection.js +32 -0
  108. package/dist/sync/index.d.ts +19 -0
  109. package/dist/sync/index.d.ts.map +1 -0
  110. package/dist/sync/index.js +9 -0
  111. package/dist/sync/matchRenames.d.ts +45 -0
  112. package/dist/sync/matchRenames.d.ts.map +1 -0
  113. package/dist/sync/matchRenames.js +85 -0
  114. package/dist/utils/TaskChain.d.ts +9 -0
  115. package/dist/utils/TaskChain.d.ts.map +1 -0
  116. package/dist/utils/TaskChain.js +14 -0
  117. package/dist/utils/contentHash.d.ts +2 -0
  118. package/dist/utils/contentHash.d.ts.map +1 -0
  119. package/dist/utils/contentHash.js +7 -0
  120. package/dist/utils/index.d.ts +3 -0
  121. package/dist/utils/index.d.ts.map +1 -0
  122. package/dist/utils/index.js +2 -0
  123. package/dist/workspace/createAssetWorkspace.d.ts +71 -0
  124. package/dist/workspace/createAssetWorkspace.d.ts.map +1 -0
  125. package/dist/workspace/createAssetWorkspace.js +72 -0
  126. package/dist/workspace/index.d.ts +5 -0
  127. package/dist/workspace/index.d.ts.map +1 -0
  128. package/dist/workspace/index.js +2 -0
  129. package/dist/workspace/seedAssetSource.d.ts +5 -0
  130. package/dist/workspace/seedAssetSource.d.ts.map +1 -0
  131. package/dist/workspace/seedAssetSource.js +20 -0
  132. package/docs/AssetBackend.md +73 -0
  133. package/docs/AssetKinds.md +204 -0
  134. package/docs/AssetWriter.md +62 -0
  135. package/docs/Catalog.md +71 -0
  136. package/docs/Rooms.md +70 -0
  137. package/docs/Sync.md +146 -0
  138. package/docs/Workspace.md +172 -0
  139. package/package.json +83 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 JollyPixel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ <h1 align="center">
2
+ asset-server
3
+ </h1>
4
+
5
+ <p align="center">
6
+ Server-side asset storage, synchronization and catalog delivery
7
+ </p>
8
+
9
+ ## 💃 Getting Started
10
+
11
+ Install the package with npm:
12
+
13
+ ```bash
14
+ $ npm i @jolly-pixel/asset-server
15
+ ```
16
+
17
+ `@jolly-pixel/asset-server` runs on the server. Browser code should use
18
+ [`@jolly-pixel/asset`](../asset) for asset records and catalogs.
19
+ Physical storage providers are supplied by
20
+ [`@jolly-pixel/asset-source`](../asset-source).
21
+
22
+ ## 👀 Usage example
23
+
24
+ ```ts
25
+ import * as EventStore from "@jolly-pixel/event-store";
26
+ import { FilesystemAssetSource } from "@jolly-pixel/asset-source";
27
+ import { Server } from "@jolly-pixel/network";
28
+ import {
29
+ createAssetBackend,
30
+ textureAssetHandler
31
+ } from "@jolly-pixel/asset-server";
32
+
33
+ using eventStore = await EventStore.persistence.sqlite(
34
+ "./assets/.jollypixel/events.db"
35
+ );
36
+
37
+ await using backend = await createAssetBackend({
38
+ source: new FilesystemAssetSource("./assets"),
39
+ eventStore,
40
+ handlers: [textureAssetHandler()]
41
+ });
42
+
43
+ await using server = new Server({ eventStore });
44
+ backend.attach(server);
45
+ ```
46
+
47
+ On startup, the backend catalogs files already present in `./assets`. Changes
48
+ made through `backend.writer` are appended to the event store and written to
49
+ the asset source. Changes made by external tools are detected and appended as
50
+ system events.
51
+
52
+ ## 📚 API
53
+
54
+ - [`AssetBackend`](./docs/AssetBackend.md): setup, options and lifecycle
55
+ - [`AssetWriter`](./docs/AssetWriter.md): create, update, rename and remove assets
56
+ - [`AssetSource`](../asset-source/docs/AssetSource.md): in-memory and
57
+ filesystem storage
58
+ - [`Asset kinds`](./docs/AssetKinds.md): custom state, serialization and editing rooms
59
+ - [`Catalog`](./docs/Catalog.md): catalog projection, network messages and HTTP access
60
+ - [`Rooms`](./docs/Rooms.md): dynamic editing rooms and eviction
61
+ - [`Sync`](./docs/Sync.md): lifecycle events, snapshots and reconciliation
62
+
63
+ ## ✨ Contributors guide
64
+
65
+ Read the [contributing guide][contributing] before submitting a change.
66
+
67
+ Run these commands from the monorepo root:
68
+
69
+ ```bash
70
+ $ npm run test -w @jolly-pixel/asset-server
71
+ $ npm run lint
72
+ ```
73
+
74
+ > [!CAUTION]
75
+ > Include tests when adding a feature or fixing a bug.
76
+
77
+ ## 📃 License
78
+
79
+ MIT
80
+
81
+ <!-- Reference-style links -->
82
+
83
+ [contributing]: ../../CONTRIBUTING.md
@@ -0,0 +1,37 @@
1
+ import { Extension, type ClientHandle, type MessageProtocols, type PeerMetadata, type RoomContext } from "@jolly-pixel/network";
2
+ import type { AssetManifestData } from "@jolly-pixel/asset";
3
+ import type { CatalogChange, CatalogProjection } from "./CatalogProjection.ts";
4
+ export declare const CATALOG_ROOM = "asset-catalog";
5
+ export declare const CATALOG_SNAPSHOT = "catalog:snapshot";
6
+ export declare const CATALOG_CHANGED = "catalog:changed";
7
+ export type CatalogMessage = {
8
+ type: typeof CATALOG_SNAPSHOT;
9
+ manifest: AssetManifestData;
10
+ } | {
11
+ type: typeof CATALOG_CHANGED;
12
+ change: CatalogChange;
13
+ };
14
+ export declare const catalogProtocols: MessageProtocols;
15
+ export interface CatalogExtensionOptions {
16
+ projection: CatalogProjection;
17
+ /**
18
+ * Room name clients join to follow the catalog.
19
+ * @default CATALOG_ROOM
20
+ */
21
+ id?: string;
22
+ }
23
+ /**
24
+ * Broadcasts the projected catalog without owning domain state.
25
+ */
26
+ export declare class CatalogExtension extends Extension {
27
+ #private;
28
+ readonly id: string;
29
+ readonly name = "asset-catalog";
30
+ readonly protocols: MessageProtocols;
31
+ constructor(options: CatalogExtensionOptions);
32
+ onClientConnect(client: ClientHandle, _identity: PeerMetadata, context: RoomContext): void;
33
+ onClientDisconnect(clientId: string): void;
34
+ onMessage(): void;
35
+ dispose(): void;
36
+ }
37
+ //# sourceMappingURL=CatalogExtension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CatalogExtension.d.ts","sourceRoot":"","sources":["../../src/catalog/CatalogExtension.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,SAAS,EAET,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAEjB,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAE5C,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AACnD,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,OAAO,gBAAgB,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAC;CAAE,GAC/D;IAAE,IAAI,EAAE,OAAO,eAAe,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;CAAE,CAAC;AAE7D,eAAO,MAAM,gBAAgB,EAAE,gBA+B9B,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,iBAAiB,CAAC;IAC9B;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;;IAC7C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,mBAAgB;IAC7B,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAoB;gBAQtD,OAAO,EAAE,uBAAuB;IAelC,eAAe,CACb,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,WAAW,GACnB,IAAI;IAUP,kBAAkB,CAChB,QAAQ,EAAE,MAAM,GACf,IAAI;IAOP,SAAS,IAAI,IAAI;IAIR,OAAO,IAAI,IAAI;CAQzB"}
@@ -0,0 +1,82 @@
1
+ // Import Third-party Dependencies
2
+ import { defineMessageProtocol, Extension, NO_MESSAGES } from "@jolly-pixel/network";
3
+ // CONSTANTS
4
+ export const CATALOG_ROOM = "asset-catalog";
5
+ export const CATALOG_SNAPSHOT = "catalog:snapshot";
6
+ export const CATALOG_CHANGED = "catalog:changed";
7
+ export const catalogProtocols = {
8
+ inbound: NO_MESSAGES,
9
+ outbound: defineMessageProtocol({
10
+ discriminator: "type",
11
+ schema: {
12
+ oneOf: [
13
+ {
14
+ type: "object",
15
+ properties: {
16
+ type: { const: CATALOG_SNAPSHOT },
17
+ manifest: { type: "object" }
18
+ },
19
+ required: [
20
+ "type",
21
+ "manifest"
22
+ ]
23
+ },
24
+ {
25
+ type: "object",
26
+ properties: {
27
+ type: { const: CATALOG_CHANGED },
28
+ change: { type: "object" }
29
+ },
30
+ required: [
31
+ "type",
32
+ "change"
33
+ ]
34
+ }
35
+ ]
36
+ }
37
+ })
38
+ };
39
+ /**
40
+ * Broadcasts the projected catalog without owning domain state.
41
+ */
42
+ export class CatalogExtension extends Extension {
43
+ id;
44
+ name = CATALOG_ROOM;
45
+ protocols = catalogProtocols;
46
+ #projection;
47
+ #broadcast = null;
48
+ #members = new Set();
49
+ #onChanged;
50
+ constructor(options) {
51
+ super();
52
+ this.id = options.id ?? CATALOG_ROOM;
53
+ this.#projection = options.projection;
54
+ this.#onChanged = (change) => this.#broadcast?.broadcast({
55
+ type: CATALOG_CHANGED,
56
+ change
57
+ });
58
+ this.#projection.on("changed", this.#onChanged);
59
+ }
60
+ onClientConnect(client, _identity, context) {
61
+ this.#broadcast = context.room;
62
+ this.#members.add(client.id);
63
+ context.room.sendTo(client.id, {
64
+ type: CATALOG_SNAPSHOT,
65
+ manifest: this.#projection.snapshot()
66
+ });
67
+ }
68
+ onClientDisconnect(clientId) {
69
+ this.#members.delete(clientId);
70
+ if (this.#members.size === 0) {
71
+ this.#broadcast = null;
72
+ }
73
+ }
74
+ onMessage() {
75
+ return void 0;
76
+ }
77
+ dispose() {
78
+ this.#projection.off("changed", this.#onChanged);
79
+ this.#members.clear();
80
+ this.#broadcast = null;
81
+ }
82
+ }
@@ -0,0 +1,52 @@
1
+ import { type Infer } from "ata-validator";
2
+ import { type AssetSource } from "@jolly-pixel/asset-source";
3
+ import { type Logger } from "../logger.ts";
4
+ declare const kEntrySchema: {
5
+ readonly type: "object";
6
+ readonly properties: {
7
+ readonly id: {
8
+ readonly type: "string";
9
+ };
10
+ readonly path: {
11
+ readonly type: "string";
12
+ };
13
+ readonly kind: {
14
+ readonly type: "string";
15
+ };
16
+ };
17
+ readonly required: readonly ["id", "path", "kind"];
18
+ };
19
+ export type IdentityEntry = Readonly<Infer<typeof kEntrySchema>>;
20
+ export interface CatalogIdentitySidecarParse {
21
+ readonly sidecar: CatalogIdentitySidecar;
22
+ readonly dropped: number;
23
+ }
24
+ export interface CatalogIdentitySidecarData {
25
+ readonly version: 1;
26
+ readonly assets: readonly IdentityEntry[];
27
+ }
28
+ /**
29
+ * Persists path-to-AssetId mappings beside the workspace.
30
+ *
31
+ * The committed half of the catalog: {@link CatalogProjection} is replayed
32
+ * from the event log and can always be rebuilt, while this file is the only
33
+ * record that survives a clone, since the log itself is never committed.
34
+ *
35
+ * An unreadable sidecar loses ids, not event data.
36
+ */
37
+ export declare class CatalogIdentitySidecar {
38
+ #private;
39
+ constructor(entries?: Iterable<IdentityEntry>);
40
+ get size(): number;
41
+ set(entry: IdentityEntry): this;
42
+ removeById(id: string): boolean;
43
+ byPath(path: string): IdentityEntry | undefined;
44
+ byId(id: string): IdentityEntry | undefined;
45
+ [Symbol.iterator](): IterableIterator<IdentityEntry>;
46
+ toJSON(): CatalogIdentitySidecarData;
47
+ save(source: AssetSource): Promise<void>;
48
+ static load(source: AssetSource, logger?: Logger): Promise<CatalogIdentitySidecar>;
49
+ static parse(input: unknown): CatalogIdentitySidecarParse;
50
+ }
51
+ export {};
52
+ //# sourceMappingURL=CatalogIdentitySidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CatalogIdentitySidecar.d.ts","sourceRoot":"","sources":["../../src/catalog/CatalogIdentitySidecar.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,KAAK,EACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAEL,KAAK,MAAM,EACZ,MAAM,cAAc,CAAC;AAMtB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;CAYhB,CAAC;AAaH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC;AAEjE,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;CAC3C;AAED;;;;;;;;GAQG;AACH,qBAAa,sBAAsB;;gBAK/B,OAAO,GAAE,QAAQ,CAAC,aAAa,CAAM;IAOvC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,GAAG,CACD,KAAK,EAAE,aAAa,GACnB,IAAI;IAsBP,UAAU,CACR,EAAE,EAAE,MAAM,GACT,OAAO;IAYV,MAAM,CACJ,IAAI,EAAE,MAAM,GACX,aAAa,GAAG,SAAS;IAI5B,IAAI,CACF,EAAE,EAAE,MAAM,GACT,aAAa,GAAG,SAAS;IAI5B,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAIpD,MAAM,IAAI,0BAA0B;IASpC,IAAI,CACF,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC;WAQH,IAAI,CACf,MAAM,EAAE,WAAW,EACnB,MAAM,GAAE,MAAuB,GAC9B,OAAO,CAAC,sBAAsB,CAAC;IAmBlC,MAAM,CAAC,KAAK,CACV,KAAK,EAAE,OAAO,GACb,2BAA2B;CA2B/B"}
@@ -0,0 +1,132 @@
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 { IDENTITY_SIDECAR_PATH } from "../constants.js";
7
+ // CONSTANTS
8
+ const kSidecarVersion = 1;
9
+ const kValidatorOptions = { useDefaults: false };
10
+ const kEntrySchema = defineSchema({
11
+ type: "object",
12
+ properties: {
13
+ id: { type: "string" },
14
+ path: { type: "string" },
15
+ kind: { type: "string" }
16
+ },
17
+ required: [
18
+ "id",
19
+ "path",
20
+ "kind"
21
+ ]
22
+ });
23
+ const kEntryValidator = new Validator(kEntrySchema, kValidatorOptions);
24
+ const kDocumentValidator = new Validator(defineSchema({
25
+ type: "object",
26
+ properties: {
27
+ assets: { type: "array" }
28
+ },
29
+ required: ["assets"]
30
+ }), kValidatorOptions);
31
+ /**
32
+ * Persists path-to-AssetId mappings beside the workspace.
33
+ *
34
+ * The committed half of the catalog: {@link CatalogProjection} is replayed
35
+ * from the event log and can always be rebuilt, while this file is the only
36
+ * record that survives a clone, since the log itself is never committed.
37
+ *
38
+ * An unreadable sidecar loses ids, not event data.
39
+ */
40
+ export class CatalogIdentitySidecar {
41
+ #byPath = new Map();
42
+ #byId = new Map();
43
+ constructor(entries = []) {
44
+ for (const entry of entries) {
45
+ this.set(entry);
46
+ }
47
+ }
48
+ get size() {
49
+ return this.#byPath.size;
50
+ }
51
+ set(entry) {
52
+ const previousPath = this.#byId.get(entry.id);
53
+ if (previousPath !== undefined) {
54
+ this.#byPath.delete(previousPath.path);
55
+ }
56
+ const previousId = this.#byPath.get(entry.path);
57
+ if (previousId !== undefined) {
58
+ this.#byId.delete(previousId.id);
59
+ }
60
+ const stored = {
61
+ id: entry.id,
62
+ path: entry.path,
63
+ kind: entry.kind
64
+ };
65
+ this.#byPath.set(stored.path, stored);
66
+ this.#byId.set(stored.id, stored);
67
+ return this;
68
+ }
69
+ removeById(id) {
70
+ const entry = this.#byId.get(id);
71
+ if (entry === undefined) {
72
+ return false;
73
+ }
74
+ this.#byId.delete(id);
75
+ this.#byPath.delete(entry.path);
76
+ return true;
77
+ }
78
+ byPath(path) {
79
+ return this.#byPath.get(path);
80
+ }
81
+ byId(id) {
82
+ return this.#byId.get(id);
83
+ }
84
+ [Symbol.iterator]() {
85
+ return this.#byId.values();
86
+ }
87
+ toJSON() {
88
+ return {
89
+ version: kSidecarVersion,
90
+ assets: [...this.#byId.values()].sort((a, b) => a.path.localeCompare(b.path))
91
+ };
92
+ }
93
+ save(source) {
94
+ return writeJsonFile(source, IDENTITY_SIDECAR_PATH, this.toJSON());
95
+ }
96
+ static async load(source, logger = silentLogger()) {
97
+ const { sidecar, dropped } = CatalogIdentitySidecar.parse(await readJsonFile(source, IDENTITY_SIDECAR_PATH));
98
+ if (dropped > 0) {
99
+ logger
100
+ .withMetadata({
101
+ path: IDENTITY_SIDECAR_PATH,
102
+ dropped
103
+ })
104
+ .warn("identity sidecar entries dropped");
105
+ }
106
+ return sidecar;
107
+ }
108
+ static parse(input) {
109
+ const document = kDocumentValidator.validate(input);
110
+ if (!document.valid) {
111
+ return {
112
+ sidecar: new CatalogIdentitySidecar(),
113
+ dropped: 0
114
+ };
115
+ }
116
+ const entries = [];
117
+ let dropped = 0;
118
+ for (const asset of document.data.assets) {
119
+ const entry = kEntryValidator.validate(asset);
120
+ if (entry.valid) {
121
+ entries.push(entry.data);
122
+ }
123
+ else {
124
+ dropped += 1;
125
+ }
126
+ }
127
+ return {
128
+ sidecar: new CatalogIdentitySidecar(entries),
129
+ dropped
130
+ };
131
+ }
132
+ }
@@ -0,0 +1,32 @@
1
+ import type * as EventStore from "@jolly-pixel/event-store";
2
+ import { AssetCatalog, type AssetManifestData, type AssetRecordData } from "@jolly-pixel/asset";
3
+ import { Emitter } from "@openally/emitt";
4
+ export interface CatalogChange {
5
+ readonly eventType: string;
6
+ readonly assetId: string;
7
+ /**
8
+ * The record after the change, or `null` when the asset was removed.
9
+ */
10
+ readonly record: AssetRecordData | null;
11
+ }
12
+ export type CatalogProjectionEventMap = {
13
+ changed: (change: CatalogChange) => void;
14
+ };
15
+ export interface CatalogProjectionOptions {
16
+ eventStore: EventStore.EventStore;
17
+ }
18
+ /**
19
+ * Projects lifecycle events into catalog records keyed by asset id.
20
+ */
21
+ export declare class CatalogProjection extends Emitter<CatalogProjectionEventMap> {
22
+ #private;
23
+ constructor(options: CatalogProjectionOptions);
24
+ get catalog(): AssetCatalog;
25
+ get size(): number;
26
+ load(): void;
27
+ start(): void;
28
+ close(): void;
29
+ apply(event: EventStore.Event): boolean;
30
+ snapshot(): AssetManifestData;
31
+ }
32
+ //# sourceMappingURL=CatalogProjection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CatalogProjection.d.ts","sourceRoot":"","sources":["../../src/catalog/CatalogProjection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,UAAU,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EACL,YAAY,EAGZ,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAc1C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,CACP,MAAM,EAAE,aAAa,KAClB,IAAI,CAAC;CACX,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC;CACnC;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,OAAO,CAC5C,yBAAyB,CAC1B;;gBAMG,OAAO,EAAE,wBAAwB;IAMnC,IAAI,OAAO,IAAI,YAAY,CAE1B;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,IAAI,IAAI;IAWZ,KAAK,IAAI,IAAI;IAOb,KAAK,IAAI,IAAI;IAMb,KAAK,CACH,KAAK,EAAE,UAAU,CAAC,KAAK,GACtB,OAAO;IAgBV,QAAQ,IAAI,iBAAiB;CAqE9B"}
@@ -0,0 +1,109 @@
1
+ import { AssetCatalog, AssetId, AssetRecord } from "@jolly-pixel/asset";
2
+ import { Emitter } from "@openally/emitt";
3
+ // Import Internal Dependencies
4
+ import { ASSET_EVENT_PREFIX } from "../constants.js";
5
+ import { ASSET_CHECKPOINT_EVENT_TYPES, ASSET_CREATED, ASSET_DELETED, ASSET_RENAMED, ASSET_UPDATED, parseAssetEvent } from "../events/AssetEvents.js";
6
+ /**
7
+ * Projects lifecycle events into catalog records keyed by asset id.
8
+ */
9
+ export class CatalogProjection extends Emitter {
10
+ #eventStore;
11
+ #catalog = new AssetCatalog();
12
+ #unsubscribe = null;
13
+ constructor(options) {
14
+ super();
15
+ this.#eventStore = options.eventStore;
16
+ }
17
+ get catalog() {
18
+ return this.#catalog;
19
+ }
20
+ get size() {
21
+ return this.#catalog.size;
22
+ }
23
+ load() {
24
+ this.#catalog = new AssetCatalog();
25
+ const events = this.#eventStore.reader.listFromCheckpoints({
26
+ checkpointEventTypes: ASSET_CHECKPOINT_EVENT_TYPES,
27
+ eventTypePrefix: ASSET_EVENT_PREFIX
28
+ });
29
+ for (const event of events) {
30
+ this.apply(event);
31
+ }
32
+ }
33
+ start() {
34
+ this.#unsubscribe ??= this.#eventStore.subscribe((event) => void this.apply(event), { eventTypePrefix: ASSET_EVENT_PREFIX });
35
+ }
36
+ close() {
37
+ this.#unsubscribe?.();
38
+ this.#unsubscribe = null;
39
+ this.removeAllListeners();
40
+ }
41
+ apply(event) {
42
+ const parsed = parseAssetEvent(event);
43
+ if (!parsed.ok) {
44
+ return false;
45
+ }
46
+ const change = this.#fold(parsed.val);
47
+ if (change === null) {
48
+ return false;
49
+ }
50
+ this.emit("changed", change);
51
+ return true;
52
+ }
53
+ snapshot() {
54
+ return this.#catalog.toJSON();
55
+ }
56
+ #fold(event) {
57
+ const id = new AssetId(event.assetId);
58
+ switch (event.eventType) {
59
+ case ASSET_CREATED:
60
+ case ASSET_UPDATED: {
61
+ const data = event.eventData;
62
+ return this.#upsert(new AssetRecord({
63
+ id,
64
+ kind: data.kind,
65
+ source: data.path,
66
+ revision: data.hash
67
+ }), event.eventType);
68
+ }
69
+ case ASSET_RENAMED: {
70
+ const data = event.eventData;
71
+ const previous = this.#catalog.has(id) ?
72
+ this.#catalog.get(id) :
73
+ null;
74
+ return this.#upsert(new AssetRecord({
75
+ id,
76
+ kind: previous?.kind ?? data.kind,
77
+ source: data.to,
78
+ revision: previous?.revision ?? data.hash
79
+ }), event.eventType);
80
+ }
81
+ case ASSET_DELETED: {
82
+ if (!this.#catalog.has(id)) {
83
+ return null;
84
+ }
85
+ this.#catalog.remove(id);
86
+ return {
87
+ eventType: event.eventType,
88
+ assetId: event.assetId,
89
+ record: null
90
+ };
91
+ }
92
+ default:
93
+ return null;
94
+ }
95
+ }
96
+ #upsert(record, eventType) {
97
+ if (this.#catalog.has(record.id)) {
98
+ this.#catalog.replace(record);
99
+ }
100
+ else {
101
+ this.#catalog.add(record);
102
+ }
103
+ return {
104
+ eventType,
105
+ assetId: record.id.value,
106
+ record: record.toJSON()
107
+ };
108
+ }
109
+ }
@@ -0,0 +1,13 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { CatalogProjection } from "./CatalogProjection.ts";
3
+ export declare const DEFAULT_CATALOG_PATH = "/__jollypixel/catalog";
4
+ export type CatalogHandler = (request: IncomingMessage, response: ServerResponse, next: () => void) => void;
5
+ export interface CatalogHandlerOptions {
6
+ projection: CatalogProjection;
7
+ /**
8
+ * @default DEFAULT_CATALOG_PATH
9
+ */
10
+ path?: string;
11
+ }
12
+ export declare function createCatalogHandler(options: CatalogHandlerOptions): CatalogHandler;
13
+ //# sourceMappingURL=httpHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"httpHandler.d.ts","sourceRoot":"","sources":["../../src/catalog/httpHandler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACf,MAAM,WAAW,CAAC;AAMnB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,eAAO,MAAM,oBAAoB,0BAAmB,CAAC;AAErD,MAAM,MAAM,cAAc,GAAG,CAC3B,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,IAAI,KACb,IAAI,CAAC;AAEV,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,iBAAiB,CAAC;IAC9B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAiDhB"}
@@ -0,0 +1,27 @@
1
+ // Import Node.js Dependencies
2
+ import { Buffer } from "node:buffer";
3
+ // Import Third-party Dependencies
4
+ import { CATALOG_URL_PATH } from "@jolly-pixel/asset";
5
+ export const DEFAULT_CATALOG_PATH = CATALOG_URL_PATH;
6
+ export function createCatalogHandler(options) {
7
+ const { projection, path = DEFAULT_CATALOG_PATH } = options;
8
+ return function catalogHandler(request, response, next) {
9
+ const requestUrl = URL.parse(request.url ?? "", "http://localhost");
10
+ if (requestUrl?.pathname !== path) {
11
+ next();
12
+ return;
13
+ }
14
+ if (request.method !== "GET" &&
15
+ request.method !== "HEAD") {
16
+ response.statusCode = 405;
17
+ response.setHeader("allow", "GET, HEAD");
18
+ response.end();
19
+ return;
20
+ }
21
+ const payload = JSON.stringify(projection.snapshot());
22
+ response.statusCode = 200;
23
+ response.setHeader("content-type", "application/json; charset=utf-8");
24
+ response.setHeader("content-length", String(Buffer.byteLength(payload)));
25
+ response.end(request.method === "HEAD" ? undefined : payload);
26
+ };
27
+ }
@@ -0,0 +1,9 @@
1
+ export { CatalogIdentitySidecar } from "./CatalogIdentitySidecar.ts";
2
+ export type { CatalogIdentitySidecarData, IdentityEntry } from "./CatalogIdentitySidecar.ts";
3
+ export { CatalogProjection } from "./CatalogProjection.ts";
4
+ export type { CatalogChange, CatalogProjectionEventMap, CatalogProjectionOptions } from "./CatalogProjection.ts";
5
+ export { CatalogExtension, CATALOG_CHANGED, CATALOG_ROOM, CATALOG_SNAPSHOT } from "./CatalogExtension.ts";
6
+ export type { CatalogExtensionOptions, CatalogMessage } from "./CatalogExtension.ts";
7
+ export { createCatalogHandler, DEFAULT_CATALOG_PATH } from "./httpHandler.ts";
8
+ export type { CatalogHandler, CatalogHandlerOptions } from "./httpHandler.ts";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/catalog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACV,0BAA0B,EAC1B,aAAa,EACd,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EACV,aAAa,EACb,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,uBAAuB,EACvB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,cAAc,EACd,qBAAqB,EACtB,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { CatalogIdentitySidecar } from "./CatalogIdentitySidecar.js";
2
+ export { CatalogProjection } from "./CatalogProjection.js";
3
+ export { CatalogExtension, CATALOG_CHANGED, CATALOG_ROOM, CATALOG_SNAPSHOT } from "./CatalogExtension.js";
4
+ export { createCatalogHandler, DEFAULT_CATALOG_PATH } from "./httpHandler.js";
@@ -0,0 +1,9 @@
1
+ import { STATE_DIRECTORY } from "@jolly-pixel/asset-source";
2
+ export { STATE_DIRECTORY };
3
+ export declare const IDENTITY_SIDECAR_PATH = ".jollypixel/assets.json";
4
+ export declare const PROJECTION_STATE_PATH = ".jollypixel/state.json";
5
+ export declare const STATE_GITIGNORE_PATH = ".jollypixel/.gitignore";
6
+ export declare const EVENTS_DB_PATH = ".jollypixel/events.db";
7
+ export declare const STATE_GITIGNORE_CONTENT = "state.json\nevents.db\nevents.db-journal\nevents.db-wal\n";
8
+ export declare const ASSET_EVENT_PREFIX = "asset.";
9
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,eAAO,MAAM,qBAAqB,4BAAmC,CAAC;AACtE,eAAO,MAAM,qBAAqB,2BAAkC,CAAC;AACrE,eAAO,MAAM,oBAAoB,2BAAkC,CAAC;AACpE,eAAO,MAAM,cAAc,0BAAiC,CAAC;AAE7D,eAAO,MAAM,uBAAuB,8DAInC,CAAC;AAEF,eAAO,MAAM,kBAAkB,WAAW,CAAC"}
@@ -0,0 +1,13 @@
1
+ // Import Third-party Dependencies
2
+ import { STATE_DIRECTORY } from "@jolly-pixel/asset-source";
3
+ export { STATE_DIRECTORY };
4
+ export const IDENTITY_SIDECAR_PATH = `${STATE_DIRECTORY}/assets.json`;
5
+ export const PROJECTION_STATE_PATH = `${STATE_DIRECTORY}/state.json`;
6
+ export const STATE_GITIGNORE_PATH = `${STATE_DIRECTORY}/.gitignore`;
7
+ export const EVENTS_DB_PATH = `${STATE_DIRECTORY}/events.db`;
8
+ export const STATE_GITIGNORE_CONTENT = `state.json
9
+ events.db
10
+ events.db-journal
11
+ events.db-wal
12
+ `;
13
+ export const ASSET_EVENT_PREFIX = "asset.";