@jini-ai/cms 0.5.8 → 0.5.10
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/CHANGELOG.md +9 -0
- package/dist/entries/repo.memory.d.ts +7 -1
- package/dist/entries/repo.memory.js +9 -0
- package/dist/forms/repo.memory.d.ts +6 -0
- package/dist/forms/repo.memory.js +7 -0
- package/dist/widgets/concurrency.d.ts +5 -0
- package/dist/widgets/concurrency.js +11 -0
- package/dist/widgets/embed-service.d.ts +3 -0
- package/dist/widgets/embed-service.js +3 -3
- package/dist/widgets/region-area-service.d.ts +3 -0
- package/dist/widgets/region-area-service.js +2 -2
- package/dist/widgets/write-service.d.ts +4 -1
- package/dist/widgets/write-service.js +3 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.5.10 — 2026-10-13
|
|
2
|
+
|
|
3
|
+
- `./entries`: `InMemoryEntryRepo.hardDelete({ workspaceId, id })` removes one workspace-scoped entry and its revisions (memory-only purge seam, not part of the port).
|
|
4
|
+
- `./forms`: `InMemoryFormSubmissionRepo.listAllForDefinition({ workspaceId, formDefinitionId })` lists a form's submissions, trashed ones included, so a host can drop their Trash rows before the purge cascade (memory-only, not part of the port).
|
|
5
|
+
|
|
6
|
+
## 0.5.9 — 2026-10-13
|
|
7
|
+
|
|
8
|
+
- `./widgets`: write, embed and region services take an optional `entryLocks` dependency, threaded into every `withEntryLock` call. New `getEntryLockState({ owner })` returns one lock state per store object, so a host that rebuilds dependency bags per request still shares one queue per store. Omitting `entryLocks` keeps the previous module-wide state.
|
|
9
|
+
|
|
1
10
|
## 0.5.8 — 2026-10-13
|
|
2
11
|
|
|
3
12
|
- New `./navigation/node` subpath: re-exports the full `./navigation` API with parse5 menu-HTML normalization bound into `createMenu`, `updateMenuTree`, `mergeMenuHtml` and `buildMenusRegistrations`. Node hosts writing HTML-mode menus should import those from `./navigation/node`.
|
|
@@ -9,7 +9,7 @@ import type { EntryRecord, EntryStatus } from "./types.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare class InMemoryEntryRepo implements EntryRepoPort, EntryListPort {
|
|
11
11
|
private readonly byId;
|
|
12
|
-
private
|
|
12
|
+
private revisions;
|
|
13
13
|
private static slugKey;
|
|
14
14
|
findBySlug(params: {
|
|
15
15
|
workspaceId: string;
|
|
@@ -22,6 +22,12 @@ export declare class InMemoryEntryRepo implements EntryRepoPort, EntryListPort {
|
|
|
22
22
|
}): Promise<EntryRecord | null>;
|
|
23
23
|
save(row: EntryRecord, options?: EntrySaveOptions): Promise<void>;
|
|
24
24
|
appendRevision(revision: EntryRevisionInput): Promise<void>;
|
|
25
|
+
/** Memory-only purge seam: removes the scoped entry and its revision history. Missing or foreign
|
|
26
|
+
* entries are unchanged. @complexity O(r) time and space over stored revisions. */
|
|
27
|
+
hardDelete(required: {
|
|
28
|
+
workspaceId: string;
|
|
29
|
+
id: string;
|
|
30
|
+
}, _optional?: Record<string, never>): Promise<void>;
|
|
25
31
|
transaction<T>({ fn }: {
|
|
26
32
|
fn: () => Promise<T>;
|
|
27
33
|
}): Promise<T>;
|
|
@@ -37,6 +37,15 @@ export class InMemoryEntryRepo {
|
|
|
37
37
|
async appendRevision(revision) {
|
|
38
38
|
this.revisions.push(revision);
|
|
39
39
|
}
|
|
40
|
+
/** Memory-only purge seam: removes the scoped entry and its revision history. Missing or foreign
|
|
41
|
+
* entries are unchanged. @complexity O(r) time and space over stored revisions. */
|
|
42
|
+
async hardDelete(required, _optional = {}) {
|
|
43
|
+
const row = this.byId.get(required.id);
|
|
44
|
+
if (!row || row.workspaceId !== required.workspaceId)
|
|
45
|
+
return;
|
|
46
|
+
this.byId.delete(required.id);
|
|
47
|
+
this.revisions = this.revisions.filter(revision => revision.workspaceId !== required.workspaceId || revision.entryId !== required.id);
|
|
48
|
+
}
|
|
40
49
|
async transaction({ fn }) {
|
|
41
50
|
return fn();
|
|
42
51
|
}
|
|
@@ -115,6 +115,12 @@ export declare class InMemoryFormSubmissionRepo implements FormSubmissionRepoPor
|
|
|
115
115
|
workspaceId: UUID;
|
|
116
116
|
formDefinitionId: UUID;
|
|
117
117
|
}): Promise<void>;
|
|
118
|
+
/** Trash-blind memory seam: enumerate a form's submissions before its purge cascade, so the host
|
|
119
|
+
* can forget their Trash index rows too. @complexity O(n) time and space over stored submissions. */
|
|
120
|
+
listAllForDefinition(required: {
|
|
121
|
+
workspaceId: UUID;
|
|
122
|
+
formDefinitionId: UUID;
|
|
123
|
+
}, _optional?: Record<string, never>): Promise<StoredFormSubmission[]>;
|
|
118
124
|
}
|
|
119
125
|
export {};
|
|
120
126
|
//# sourceMappingURL=repo.memory.d.ts.map
|
|
@@ -184,5 +184,12 @@ export class InMemoryFormSubmissionRepo {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
/** Trash-blind memory seam: enumerate a form's submissions before its purge cascade, so the host
|
|
188
|
+
* can forget their Trash index rows too. @complexity O(n) time and space over stored submissions. */
|
|
189
|
+
async listAllForDefinition(required, _optional = {}) {
|
|
190
|
+
return [...this.rows.values()]
|
|
191
|
+
.filter(row => row.workspaceId === required.workspaceId && row.formDefinitionId === required.formDefinitionId)
|
|
192
|
+
.map(cloneSubmission);
|
|
193
|
+
}
|
|
187
194
|
}
|
|
188
195
|
//# sourceMappingURL=repo.memory.js.map
|
|
@@ -22,6 +22,11 @@ export type EntryLockState = Map<string, Promise<void>>;
|
|
|
22
22
|
/** A host can own this state; the compatibility adapter still serializes existing callers.
|
|
23
23
|
* Idle keys are removed by withEntryLock. @complexity O(1). */
|
|
24
24
|
export declare function createEntryLockState(_required: Record<string, never>, _optional?: Record<string, never>): EntryLockState;
|
|
25
|
+
/** Repeated dependency composition for the same store retains one lock queue.
|
|
26
|
+
* The weak owner reference lets disposed stores and their idle state be collected. @complexity O(1). */
|
|
27
|
+
export declare function getEntryLockState({ owner }: {
|
|
28
|
+
readonly owner: object;
|
|
29
|
+
}, _optional?: Record<string, never>): EntryLockState;
|
|
25
30
|
/**
|
|
26
31
|
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
27
32
|
* callers queue in call order, each waiting for the previous holder to finish.
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
export function createEntryLockState(_required, _optional = {}) {
|
|
4
4
|
return new Map();
|
|
5
5
|
}
|
|
6
|
+
const ownedLocks = new WeakMap();
|
|
7
|
+
/** Repeated dependency composition for the same store retains one lock queue.
|
|
8
|
+
* The weak owner reference lets disposed stores and their idle state be collected. @complexity O(1). */
|
|
9
|
+
export function getEntryLockState({ owner }, _optional = {}) {
|
|
10
|
+
let locks = ownedLocks.get(owner);
|
|
11
|
+
if (!locks) {
|
|
12
|
+
locks = createEntryLockState({});
|
|
13
|
+
ownedLocks.set(owner, locks);
|
|
14
|
+
}
|
|
15
|
+
return locks;
|
|
16
|
+
}
|
|
6
17
|
const defaultLocks = createEntryLockState({});
|
|
7
18
|
/**
|
|
8
19
|
* Runs `fn` exclusively with respect to every other `withEntryLock` call sharing the same `key` —
|
|
@@ -29,7 +29,10 @@ import type { ContentTypeRepoPort } from "../content-types/index.js";
|
|
|
29
29
|
import type { EntryRecord, EntryRepoPort } from "../entries/index.js";
|
|
30
30
|
import type { WidgetHostPorts, BeforeSaveHookPort, PostRecord, ForgetRemovedPostFn, PostRepoPort } from "./host-ports.js";
|
|
31
31
|
import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
|
|
32
|
+
import { type EntryLockState } from "./concurrency.js";
|
|
32
33
|
export interface EmbedServiceDeps {
|
|
34
|
+
/** One store-owned state shared by write, embed and region services; omission preserves compatibility. */
|
|
35
|
+
entryLocks?: EntryLockState;
|
|
33
36
|
host: Pick<WidgetHostPorts, "entries" | "entryRefs" | "posts">;
|
|
34
37
|
entryRepo: EntryRepoPort;
|
|
35
38
|
contentTypeRepo: ContentTypeRepoPort;
|
|
@@ -381,7 +381,7 @@ export async function insertWidgetEmbed(required, _optional = {}) {
|
|
|
381
381
|
? await writeHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson)
|
|
382
382
|
: await writePostHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson);
|
|
383
383
|
return { entry, placementId };
|
|
384
|
-
} });
|
|
384
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
385
385
|
}
|
|
386
386
|
/** REQ-44/45: removes the `widgetEmbed` node matching `placementId` from the host entry's body —
|
|
387
387
|
* the placement itself is removed (the widget instance it referenced is untouched). */
|
|
@@ -407,7 +407,7 @@ export async function removeWidgetEmbed(required, _optional = {}) {
|
|
|
407
407
|
? await writeHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson)
|
|
408
408
|
: await writePostHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson);
|
|
409
409
|
return { entry };
|
|
410
|
-
} });
|
|
410
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
411
411
|
}
|
|
412
412
|
export class WidgetEmbedReorderCountMismatchError extends Error {
|
|
413
413
|
expectedCount;
|
|
@@ -455,6 +455,6 @@ export async function reorderWidgetEmbeds(required, _optional = {}) {
|
|
|
455
455
|
? await writeHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson)
|
|
456
456
|
: await writePostHostBody(deps, input.workspaceId, input.actor, host.record, input.baseVersion, nextBodyJson);
|
|
457
457
|
return { entry };
|
|
458
|
-
} });
|
|
458
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
459
459
|
}
|
|
460
460
|
//# sourceMappingURL=embed-service.js.map
|
|
@@ -24,9 +24,12 @@ import type { EntryRefsRepoPort } from "./ports.js";
|
|
|
24
24
|
import type { ContentTypeRepoPort } from "../content-types/index.js";
|
|
25
25
|
import type { EntryListPort, EntryRepoPort } from "../entries/index.js";
|
|
26
26
|
import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
|
|
27
|
+
import { type EntryLockState } from "./concurrency.js";
|
|
27
28
|
import type { WidgetRegionBindingRepoPort } from "./ports.js";
|
|
28
29
|
import type { WidgetAreaEntry, WidgetPlacementNode, WidgetRegionKey } from "./types.js";
|
|
29
30
|
export interface RegionAreaServiceDeps {
|
|
31
|
+
/** One store-owned state shared by write, embed and region services; omission preserves compatibility. */
|
|
32
|
+
entryLocks?: EntryLockState;
|
|
30
33
|
host: Pick<WidgetHostPorts, "entries" | "contentTypes" | "entryRefs">;
|
|
31
34
|
entryRepo: EntryRepoPort & EntryListPort;
|
|
32
35
|
contentTypeRepo: ContentTypeRepoPort;
|
|
@@ -78,7 +78,7 @@ export async function bindWidgetArea(required, _optional = {}) {
|
|
|
78
78
|
updatedAt: clockNowIso({ clock: deps.clock }),
|
|
79
79
|
});
|
|
80
80
|
return { areaEntry: toWidgetAreaEntry({ entry: entry }) };
|
|
81
|
-
} });
|
|
81
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
82
82
|
}
|
|
83
83
|
/** REQ-16/AC-10: every referenced widgetEntryId must exist, be a live (non-trashed) widget instance,
|
|
84
84
|
* in this same workspace — checked before any write. Throws the first violation found. */
|
|
@@ -142,7 +142,7 @@ export async function mutateWidgetAreaPlacements(required, _optional = {}) {
|
|
|
142
142
|
updatedAt: clockNowIso({ clock: deps.clock }),
|
|
143
143
|
});
|
|
144
144
|
return { areaEntry: toWidgetAreaEntry({ entry: result.value.entry }) };
|
|
145
|
-
} });
|
|
145
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
148
|
* REQ-12/14: reconciles `widget_region_bindings` from the authoritative `widget_area` entries. The
|
|
@@ -29,10 +29,13 @@ import type { EntryRefsRepoPort } from "./ports.js";
|
|
|
29
29
|
import type { ContentTypeRepoPort } from "../content-types/index.js";
|
|
30
30
|
import type { EntryListPort, EntryRepoPort } from "../entries/index.js";
|
|
31
31
|
import { type WidgetsAuthorizeFn } from "./authorize-helper.js";
|
|
32
|
+
import { type EntryLockState } from "./concurrency.js";
|
|
32
33
|
import type { RemoveWidgetFn } from "./ports.js";
|
|
33
34
|
import type { WidgetInstanceEntry, WidgetTypeKey } from "./types.js";
|
|
34
35
|
import type { WidgetHostPorts } from "./host-ports.js";
|
|
35
36
|
export interface WidgetWriteServiceDeps {
|
|
37
|
+
/** One store-owned state shared by write, embed and region services; omission preserves compatibility. */
|
|
38
|
+
entryLocks?: EntryLockState;
|
|
36
39
|
host: Pick<WidgetHostPorts, "entries" | "contentTypes" | "entryRefs" | "slugify">;
|
|
37
40
|
entryRepo: EntryRepoPort & EntryListPort;
|
|
38
41
|
contentTypeRepo: ContentTypeRepoPort;
|
|
@@ -128,7 +131,7 @@ export interface WidgetTrashDeps extends WidgetWriteServiceDeps {
|
|
|
128
131
|
remove: RemoveWidgetFn;
|
|
129
132
|
}
|
|
130
133
|
export interface TrashWidgetInstanceRequired {
|
|
131
|
-
deps: Pick<WidgetTrashDeps, "entryRepo" | "clock" | "authorize" | "remove">;
|
|
134
|
+
deps: Pick<WidgetTrashDeps, "entryRepo" | "clock" | "authorize" | "remove" | "entryLocks">;
|
|
132
135
|
input: TrashWidgetInstanceInput;
|
|
133
136
|
}
|
|
134
137
|
/**
|
|
@@ -135,7 +135,7 @@ export async function updateWidgetInstance(required, _optional = {}) {
|
|
|
135
135
|
throw result.error;
|
|
136
136
|
}
|
|
137
137
|
return { instance: toWidgetInstanceEntry({ entry: result.value.entry }) };
|
|
138
|
-
} });
|
|
138
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
141
|
* Publish-content's widget write: create-or-update at a caller-given id, with the same permission,
|
|
@@ -182,7 +182,7 @@ export async function importWidgetInstance(required, _optional = {}) {
|
|
|
182
182
|
if (!result.ok)
|
|
183
183
|
throw result.error;
|
|
184
184
|
return { instance: toWidgetInstanceEntry({ entry: result.value.entry }) };
|
|
185
|
-
} });
|
|
185
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
188
|
* Deleting a widget = moving it to the Trash (the injected `remove`). UNCONDITIONAL, per ADR-047 §7
|
|
@@ -226,7 +226,7 @@ export async function trashWidgetInstance(required, _optional = {}) {
|
|
|
226
226
|
if (!fresh)
|
|
227
227
|
throw notFound();
|
|
228
228
|
throw new WidgetVersionConflictError({ message: `widget instance '${input.widgetInstanceId}' changed while it was being deleted — reload and try again`, currentVersion: fresh.version });
|
|
229
|
-
} });
|
|
229
|
+
} }, deps.entryLocks ? { locks: deps.entryLocks } : {});
|
|
230
230
|
}
|
|
231
231
|
/** The payload statuses the old delete ladder left behind: `trash` (step 1) and `purged` (its
|
|
232
232
|
* "delete permanently", which never removed the row). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/cms",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -419,8 +419,8 @@
|
|
|
419
419
|
"@electric-sql/pglite": "0.5.8",
|
|
420
420
|
"jsdom": "^25.0.1",
|
|
421
421
|
"@jini-ai/core": "0.4.4",
|
|
422
|
-
"@jini-ai/
|
|
423
|
-
"@jini-ai/
|
|
422
|
+
"@jini-ai/db": "0.3.3",
|
|
423
|
+
"@jini-ai/http-kit": "0.4.1"
|
|
424
424
|
},
|
|
425
425
|
"dependencies": {
|
|
426
426
|
"@jini-ai/core": "^0.4.4"
|