@musnows/scriverse 0.4.9 → 0.4.11

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/dist/app.js CHANGED
@@ -24,10 +24,10 @@ import { assertSafeImportedPlainText, decodeUtf8ImportedText } from "./import-se
24
24
  import { InvalidRasterImageError, readRasterImageMetadata } from "./image-metadata.js";
25
25
  import { createRequestLoggingMiddleware, sanitizeRequestPath } from "./http-logging.js";
26
26
  import { accountReference, logger, sanitizeError } from "./logger.js";
27
- import { runWithRequestActor } from "./request-context.js";
27
+ import { currentRequestActor, runWithRequestActor } from "./request-context.js";
28
28
  import { APP_VERSION } from "./version.js";
29
29
  import { fullWorkModulePermissions, proseReplacementPermissionModules } from "./work-permissions.js";
30
- import { CollaborationPresence, presencePageKinds } from "./collaboration-presence.js";
30
+ import { CollaborationPresence, editorPageKey, entityEditorPageKey, modulePageKey, pageLabelForKey, presencePageKinds } from "./collaboration-presence.js";
31
31
  import { clearSessionCookie, createCliApiScopeMiddleware, createUserSessionMiddleware, createWorkAuthorizationMiddleware, setSessionCookie, UserAuthService } from "./user-auth.js";
32
32
  const nonEmpty = z.string().trim().min(1);
33
33
  const identifier = z.string().trim().min(1).max(200);
@@ -423,6 +423,15 @@ export function createRuntime(options) {
423
423
  mkdirSync(attachmentStorage.temporaryDirectory, { recursive: true, mode: 0o700 });
424
424
  const auth = new UserAuthService(database);
425
425
  const collaborationPresence = new CollaborationPresence();
426
+ const publishCollaborativeChange = (workId, pageKey, label = pageLabelForKey(pageKey)) => {
427
+ const actor = currentRequestActor();
428
+ if (!actor || !workId || !pageKey)
429
+ return;
430
+ collaborationPresence.publishChange(workId, pageKey, {
431
+ userId: actor.userId,
432
+ displayName: actor.displayName
433
+ }, label);
434
+ };
426
435
  const getDevelopmentUser = () => options.devAuthBypass
427
436
  ? auth.listUsers().find((user) => user.status === "active") ?? null
428
437
  : null;
@@ -792,11 +801,15 @@ export function createRuntime(options) {
792
801
  app.patch("/api/chapters/:chapterId", (request, response) => {
793
802
  const input = parse(z.object({ title: nonEmpty.max(300).optional(), content: z.string().max(2_000_000).optional(), excludedFromAnalysis: z.boolean().optional(), chapterType: chapterTypeSchema.optional(), source: z.enum(["manual", "auto"]).optional(), changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
794
803
  const { source, changeNote, expectedVersionNo, ...chapterInput } = input;
795
- data(response, store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo));
804
+ const chapter = store.saveChapter(request.params.chapterId, chapterInput, source ?? "manual", null, changeNote, expectedVersionNo);
805
+ publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
806
+ data(response, chapter);
796
807
  });
797
808
  app.delete("/api/chapters/:chapterId", (request, response) => {
798
809
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
810
+ const chapter = store.getChapter(request.params.chapterId);
799
811
  store.deleteChapter(request.params.chapterId, input.expectedVersionNo);
812
+ publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
800
813
  noContent(response);
801
814
  });
802
815
  app.get("/api/chapters/:chapterId/versions", (request, response) => {
@@ -809,7 +822,9 @@ export function createRuntime(options) {
809
822
  });
810
823
  app.post("/api/chapters/:chapterId/restore", (request, response) => {
811
824
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
812
- data(response, store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo));
825
+ const chapter = store.restoreChapter(request.params.chapterId, input.versionNo, input.expectedVersionNo);
826
+ publishCollaborativeChange(String(chapter.workId), editorPageKey(String(chapter.id)));
827
+ data(response, chapter);
813
828
  });
814
829
  app.post("/api/chapters/:chapterId/move", (request, response) => {
815
830
  const input = parse(z.object({ volumeId: identifier, sortOrder: z.number().int().min(0), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
@@ -823,11 +838,15 @@ export function createRuntime(options) {
823
838
  app.get("/api/chapters/:chapterId/outline", (request, response) => data(response, store.getChapterOutline(request.params.chapterId)));
824
839
  app.put("/api/chapters/:chapterId/outline", (request, response) => {
825
840
  const { changeNote, expectedVersionNo, ...input } = parse(chapterOutlineSchema.extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
826
- data(response, store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo));
841
+ const outline = store.upsertChapterOutline(request.params.chapterId, input, "manual", null, changeNote, expectedVersionNo);
842
+ publishCollaborativeChange(String(outline.workId), modulePageKey("outlines"));
843
+ data(response, outline);
827
844
  });
828
845
  app.delete("/api/chapters/:chapterId/outline", (request, response) => {
829
846
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
847
+ const chapter = store.getChapter(request.params.chapterId);
830
848
  store.deleteChapterOutline(request.params.chapterId, input.expectedVersionNo);
849
+ publishCollaborativeChange(String(chapter.workId), modulePageKey("outlines"));
831
850
  noContent(response);
832
851
  });
833
852
  app.get("/api/works/:workId/foreshadows", (request, response) => {
@@ -841,31 +860,43 @@ export function createRuntime(options) {
841
860
  : store.listForeshadows(request.params.workId, query.status, query.currentChapterId));
842
861
  });
843
862
  app.post("/api/works/:workId/foreshadows", (request, response) => {
844
- data(response, store.createForeshadow(request.params.workId, parse(foreshadowSchema, request.body)), 201);
863
+ const foreshadow = store.createForeshadow(request.params.workId, parse(foreshadowSchema, request.body));
864
+ publishCollaborativeChange(request.params.workId, modulePageKey("outlines"));
865
+ data(response, foreshadow, 201);
845
866
  });
846
867
  app.get("/api/foreshadows/:foreshadowId", (request, response) => data(response, store.getForeshadow(request.params.foreshadowId)));
847
868
  app.patch("/api/foreshadows/:foreshadowId", (request, response) => {
848
869
  const { changeNote, expectedVersionNo, ...input } = parse(foreshadowSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
849
- data(response, store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo));
870
+ const foreshadow = store.updateForeshadow(request.params.foreshadowId, input, "manual", null, changeNote, expectedVersionNo);
871
+ publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
872
+ data(response, foreshadow);
850
873
  });
851
874
  app.delete("/api/foreshadows/:foreshadowId", (request, response) => {
852
875
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
876
+ const foreshadow = store.getForeshadow(request.params.foreshadowId);
853
877
  store.deleteForeshadow(request.params.foreshadowId, input.expectedVersionNo);
878
+ publishCollaborativeChange(String(foreshadow.workId), modulePageKey("outlines"));
854
879
  noContent(response);
855
880
  });
856
881
  app.post("/api/foreshadows/:foreshadowId/occurrences", (request, response) => {
857
882
  const input = parse(foreshadowOccurrenceSchema.extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
858
883
  const { expectedVersionNo, ...occurrenceInput } = input;
859
- data(response, store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo), 201);
884
+ const occurrence = store.createForeshadowOccurrence(request.params.foreshadowId, occurrenceInput, expectedVersionNo);
885
+ publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
886
+ data(response, occurrence, 201);
860
887
  });
861
888
  app.patch("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
862
889
  const input = parse(foreshadowOccurrenceSchema.partial().extend({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
863
890
  const { expectedVersionNo, ...occurrenceInput } = input;
864
- data(response, store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo));
891
+ const occurrence = store.updateForeshadowOccurrence(request.params.occurrenceId, occurrenceInput, expectedVersionNo);
892
+ publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
893
+ data(response, occurrence);
865
894
  });
866
895
  app.delete("/api/foreshadow-occurrences/:occurrenceId", (request, response) => {
867
896
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
897
+ const occurrence = store.getForeshadowOccurrence(request.params.occurrenceId);
868
898
  store.deleteForeshadowOccurrence(request.params.occurrenceId, input.expectedVersionNo);
899
+ publishCollaborativeChange(String(occurrence.workId), modulePageKey("outlines"));
869
900
  noContent(response);
870
901
  });
871
902
  app.get("/api/works/:workId/settings", (request, response) => {
@@ -873,16 +904,24 @@ export function createRuntime(options) {
873
904
  const includeContent = request.query.includeContent === "true";
874
905
  data(response, pagination ? store.listSettingsPage(request.params.workId, pagination, includeContent) : store.listSettings(request.params.workId, includeContent));
875
906
  });
876
- app.post("/api/works/:workId/settings", (request, response) => data(response, store.createSetting(request.params.workId, parse(settingSchema, request.body)), 201));
907
+ app.post("/api/works/:workId/settings", (request, response) => {
908
+ const setting = store.createSetting(request.params.workId, parse(settingSchema, request.body));
909
+ publishCollaborativeChange(request.params.workId, modulePageKey("settings"));
910
+ data(response, setting, 201);
911
+ });
877
912
  app.get("/api/works/:workId/settings/context", (request, response) => data(response, store.listSettings(request.params.workId, true)));
878
913
  app.get("/api/settings/:settingId", (request, response) => data(response, store.getSetting(request.params.settingId)));
879
914
  app.patch("/api/settings/:settingId", (request, response) => {
880
915
  const { changeNote, expectedVersionNo, ...input } = parse(settingSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
881
- data(response, store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo));
916
+ const setting = store.updateSetting(request.params.settingId, input, "manual", null, changeNote, expectedVersionNo);
917
+ publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
918
+ data(response, setting);
882
919
  });
883
920
  app.delete("/api/settings/:settingId", (request, response) => {
884
921
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
922
+ const setting = store.getSetting(request.params.settingId);
885
923
  store.deleteSetting(request.params.settingId, input.expectedVersionNo);
924
+ publishCollaborativeChange(String(setting.workId), entityEditorPageKey("setting", String(setting.id)));
886
925
  noContent(response);
887
926
  });
888
927
  app.get("/api/works/:workId/characters", (request, response) => {
@@ -900,6 +939,7 @@ export function createRuntime(options) {
900
939
  });
901
940
  app.post("/api/works/:workId/characters", (request, response) => {
902
941
  const character = store.createCharacter(request.params.workId, parse(characterSchema, request.body));
942
+ publishCollaborativeChange(request.params.workId, modulePageKey("characters"));
903
943
  data(response, redactCharacterLinks(character, requestPermissions(request, request.params.workId)), 201);
904
944
  });
905
945
  app.get("/api/characters/:characterId", (request, response) => {
@@ -908,6 +948,7 @@ export function createRuntime(options) {
908
948
  app.patch("/api/characters/:characterId", (request, response) => {
909
949
  const { changeNote, expectedVersionNo, ...input } = parse(characterUpdateSchema.extend({ expectedVersionNo: expectedVersionNoSchema }), request.body);
910
950
  const character = store.updateCharacter(request.params.characterId, input, "manual", null, changeNote, expectedVersionNo);
951
+ publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
911
952
  data(response, redactCharacterLinks(character, requestPermissions(request)));
912
953
  });
913
954
  app.get("/api/characters/:characterId/versions", (request, response) => {
@@ -919,11 +960,14 @@ export function createRuntime(options) {
919
960
  app.post("/api/characters/:characterId/restore", (request, response) => {
920
961
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
921
962
  const character = store.restoreCharacter(request.params.characterId, input.versionNo, input.expectedVersionNo);
963
+ publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
922
964
  data(response, redactCharacterLinks(character, requestPermissions(request)));
923
965
  });
924
966
  app.delete("/api/characters/:characterId", (request, response) => {
925
967
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
968
+ const character = store.getCharacter(request.params.characterId);
926
969
  store.deleteCharacter(request.params.characterId, input.expectedVersionNo);
970
+ publishCollaborativeChange(String(character.workId), entityEditorPageKey("character", String(character.id)));
927
971
  noContent(response);
928
972
  });
929
973
  app.post("/api/characters/:characterId/merge", (request, response) => {
@@ -947,18 +991,24 @@ export function createRuntime(options) {
947
991
  : store.listCharacterProfileSections(request.params.characterId));
948
992
  });
949
993
  app.post("/api/characters/:characterId/sections", (request, response) => {
950
- data(response, store.createCharacterProfileSection(request.params.characterId, parse(characterProfileSectionSchema, request.body)), 201);
994
+ const section = store.createCharacterProfileSection(request.params.characterId, parse(characterProfileSectionSchema, request.body));
995
+ publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
996
+ data(response, section, 201);
951
997
  });
952
998
  app.get("/api/character-sections/:sectionId", (request, response) => {
953
999
  data(response, store.getCharacterProfileSection(request.params.sectionId));
954
1000
  });
955
1001
  app.patch("/api/character-sections/:sectionId", (request, response) => {
956
1002
  const { changeNote, expectedVersionNo, ...input } = parse(characterProfileSectionSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
957
- data(response, store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo));
1003
+ const section = store.updateCharacterProfileSection(request.params.sectionId, input, "manual", null, changeNote, expectedVersionNo);
1004
+ publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1005
+ data(response, section);
958
1006
  });
959
1007
  app.delete("/api/character-sections/:sectionId", (request, response) => {
960
1008
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1009
+ const section = store.getCharacterProfileSection(request.params.sectionId);
961
1010
  store.deleteCharacterProfileSection(request.params.sectionId, input.expectedVersionNo);
1011
+ publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
962
1012
  noContent(response);
963
1013
  });
964
1014
  app.get("/api/character-sections/:sectionId/versions", (request, response) => {
@@ -969,7 +1019,9 @@ export function createRuntime(options) {
969
1019
  });
970
1020
  app.post("/api/character-sections/:sectionId/restore", (request, response) => {
971
1021
  const input = parse(z.object({ versionNo: z.number().int().positive(), expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
972
- data(response, store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo));
1022
+ const section = store.restoreCharacterProfileSection(request.params.sectionId, input.versionNo, input.expectedVersionNo);
1023
+ publishCollaborativeChange(String(section.workId), entityEditorPageKey("character", String(section.characterId)));
1024
+ data(response, section);
973
1025
  });
974
1026
  app.get("/api/works/:workId/attachments", (request, response) => {
975
1027
  const pagination = parsePagination(request.query);
@@ -1025,6 +1077,7 @@ export function createRuntime(options) {
1025
1077
  });
1026
1078
  app.post("/api/works/:workId/races", (request, response) => {
1027
1079
  const race = store.createRace(request.params.workId, parse(raceSchema, request.body));
1080
+ publishCollaborativeChange(request.params.workId, modulePageKey("races"));
1028
1081
  data(response, redactRaceMembers(race, requestPermissions(request, request.params.workId)), 201);
1029
1082
  });
1030
1083
  app.get("/api/races/:raceId", (request, response) => {
@@ -1033,11 +1086,14 @@ export function createRuntime(options) {
1033
1086
  app.patch("/api/races/:raceId", (request, response) => {
1034
1087
  const { changeNote, expectedVersionNo, ...input } = parse(raceSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1035
1088
  const race = store.updateRace(request.params.raceId, input, "manual", null, changeNote, expectedVersionNo);
1089
+ publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1036
1090
  data(response, redactRaceMembers(race, requestPermissions(request)));
1037
1091
  });
1038
1092
  app.delete("/api/races/:raceId", (request, response) => {
1039
1093
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1094
+ const race = store.getRace(request.params.raceId);
1040
1095
  store.deleteRace(request.params.raceId, input.expectedVersionNo);
1096
+ publishCollaborativeChange(String(race.workId), entityEditorPageKey("race", String(race.id)));
1041
1097
  noContent(response);
1042
1098
  });
1043
1099
  app.post("/api/races/:raceId/merge", (request, response) => {
@@ -1055,6 +1111,7 @@ export function createRuntime(options) {
1055
1111
  });
1056
1112
  app.post("/api/works/:workId/organizations", (request, response) => {
1057
1113
  const organization = store.createOrganization(request.params.workId, parse(organizationSchema, request.body));
1114
+ publishCollaborativeChange(request.params.workId, modulePageKey("organizations"));
1058
1115
  data(response, redactOrganizationMembers(organization, requestPermissions(request, request.params.workId)), 201);
1059
1116
  });
1060
1117
  app.get("/api/organizations/:organizationId", (request, response) => {
@@ -1063,11 +1120,14 @@ export function createRuntime(options) {
1063
1120
  app.patch("/api/organizations/:organizationId", (request, response) => {
1064
1121
  const { changeNote, expectedVersionNo, ...input } = parse(organizationSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1065
1122
  const organization = store.updateOrganization(request.params.organizationId, input, "manual", null, changeNote, expectedVersionNo);
1123
+ publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1066
1124
  data(response, redactOrganizationMembers(organization, requestPermissions(request)));
1067
1125
  });
1068
1126
  app.delete("/api/organizations/:organizationId", (request, response) => {
1069
1127
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1128
+ const organization = store.getOrganization(request.params.organizationId);
1070
1129
  store.deleteOrganization(request.params.organizationId, input.expectedVersionNo);
1130
+ publishCollaborativeChange(String(organization.workId), entityEditorPageKey("organization", String(organization.id)));
1071
1131
  noContent(response);
1072
1132
  });
1073
1133
  app.post("/api/organizations/:organizationId/merge", (request, response) => {
@@ -1080,22 +1140,34 @@ export function createRuntime(options) {
1080
1140
  const pagination = parsePagination(request.query);
1081
1141
  data(response, pagination ? store.listTimelineTracksPage(request.params.workId, pagination) : store.listTimelineTracks(request.params.workId));
1082
1142
  });
1083
- app.post("/api/works/:workId/timeline-tracks", (request, response) => data(response, store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body)), 201));
1143
+ app.post("/api/works/:workId/timeline-tracks", (request, response) => {
1144
+ const track = store.createTimelineTrack(request.params.workId, parse(timelineTrackSchema, request.body));
1145
+ publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1146
+ data(response, track, 201);
1147
+ });
1084
1148
  app.get("/api/timeline-tracks/:trackId", (request, response) => data(response, store.getTimelineTrack(request.params.trackId)));
1085
1149
  app.patch("/api/timeline-tracks/:trackId", (request, response) => {
1086
1150
  const { changeNote, expectedVersionNo, ...input } = parse(timelineTrackSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1087
- data(response, store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo));
1151
+ const track = store.updateTimelineTrack(request.params.trackId, input, "manual", null, changeNote, expectedVersionNo);
1152
+ publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1153
+ data(response, track);
1088
1154
  });
1089
1155
  app.delete("/api/timeline-tracks/:trackId", (request, response) => {
1090
1156
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1157
+ const track = store.getTimelineTrack(request.params.trackId);
1091
1158
  store.deleteTimelineTrack(request.params.trackId, input.expectedVersionNo);
1159
+ publishCollaborativeChange(String(track.workId), modulePageKey("timeline"));
1092
1160
  noContent(response);
1093
1161
  });
1094
1162
  app.get("/api/works/:workId/timeline", (request, response) => {
1095
1163
  const pagination = parsePagination(request.query);
1096
1164
  data(response, pagination ? store.listTimelineEventsPage(request.params.workId, pagination) : store.listTimelineEvents(request.params.workId));
1097
1165
  });
1098
- app.post("/api/works/:workId/timeline", (request, response) => data(response, store.createTimelineEvent(request.params.workId, parse(timelineSchema, request.body)), 201));
1166
+ app.post("/api/works/:workId/timeline", (request, response) => {
1167
+ const event = store.createTimelineEvent(request.params.workId, parse(timelineSchema, request.body));
1168
+ publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1169
+ data(response, event, 201);
1170
+ });
1099
1171
  app.post("/api/works/:workId/timeline/merge", (request, response) => {
1100
1172
  const input = parse(z.object({
1101
1173
  eventIds: z.array(identifier).min(2),
@@ -1106,12 +1178,16 @@ export function createRuntime(options) {
1106
1178
  expectedVersionNos: z.record(identifier, z.number().int().positive()).optional()
1107
1179
  }).strict(), request.body);
1108
1180
  const { expectedVersionNos, ...mergeInput } = input;
1109
- data(response, store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos), 201);
1181
+ const merged = store.mergeTimelineEvents(request.params.workId, input.eventIds, mergeInput, expectedVersionNos);
1182
+ publishCollaborativeChange(request.params.workId, modulePageKey("timeline"));
1183
+ data(response, merged, 201);
1110
1184
  });
1111
1185
  app.get("/api/timeline/:eventId", (request, response) => data(response, store.getTimelineEvent(request.params.eventId)));
1112
1186
  app.patch("/api/timeline/:eventId", (request, response) => {
1113
1187
  const { changeNote, expectedVersionNo, ...input } = parse(timelineSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1114
- data(response, store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo));
1188
+ const event = store.updateTimelineEvent(request.params.eventId, input, "manual", null, changeNote, expectedVersionNo);
1189
+ publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1190
+ data(response, event);
1115
1191
  });
1116
1192
  app.post("/api/timeline/:eventId/split", (request, response) => {
1117
1193
  const input = parse(z.object({
@@ -1123,11 +1199,17 @@ export function createRuntime(options) {
1123
1199
  })).min(2),
1124
1200
  expectedVersionNo: expectedVersionNoSchema
1125
1201
  }).strict(), request.body);
1126
- data(response, store.splitTimelineEvent(request.params.eventId, input.parts, input.expectedVersionNo), 201);
1202
+ const split = store.splitTimelineEvent(request.params.eventId, input.parts, input.expectedVersionNo);
1203
+ const first = split[0];
1204
+ if (first)
1205
+ publishCollaborativeChange(String(first.workId), modulePageKey("timeline"));
1206
+ data(response, split, 201);
1127
1207
  });
1128
1208
  app.delete("/api/timeline/:eventId", (request, response) => {
1129
1209
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1210
+ const event = store.getTimelineEvent(request.params.eventId);
1130
1211
  store.deleteTimelineEvent(request.params.eventId, input.expectedVersionNo);
1212
+ publishCollaborativeChange(String(event.workId), modulePageKey("timeline"));
1131
1213
  noContent(response);
1132
1214
  });
1133
1215
  app.get("/api/works/:workId/relationships", (request, response) => {
@@ -1139,15 +1221,23 @@ export function createRuntime(options) {
1139
1221
  ? store.listRelationshipsPage(request.params.workId, pagination, confidence)
1140
1222
  : store.listRelationships(request.params.workId, confidence));
1141
1223
  });
1142
- app.post("/api/works/:workId/relationships", (request, response) => data(response, store.createRelationship(request.params.workId, parse(relationshipSchema, request.body)), 201));
1224
+ app.post("/api/works/:workId/relationships", (request, response) => {
1225
+ const relationship = store.createRelationship(request.params.workId, parse(relationshipSchema, request.body));
1226
+ publishCollaborativeChange(request.params.workId, modulePageKey("relationships"));
1227
+ data(response, relationship, 201);
1228
+ });
1143
1229
  app.get("/api/relationships/:relationshipId", (request, response) => data(response, store.getRelationship(request.params.relationshipId)));
1144
1230
  app.patch("/api/relationships/:relationshipId", (request, response) => {
1145
1231
  const { changeNote, expectedVersionNo, ...input } = parse(relationshipSchema.partial().extend({ changeNote: changeNoteSchema, expectedVersionNo: expectedVersionNoSchema }).strict(), request.body);
1146
- data(response, store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo));
1232
+ const relationship = store.updateRelationship(request.params.relationshipId, input, "manual", null, changeNote, expectedVersionNo);
1233
+ publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1234
+ data(response, relationship);
1147
1235
  });
1148
1236
  app.delete("/api/relationships/:relationshipId", (request, response) => {
1149
1237
  const input = parse(z.object({ expectedVersionNo: expectedVersionNoSchema }).strict(), request.body ?? {});
1238
+ const relationship = store.getRelationship(request.params.relationshipId);
1150
1239
  store.deleteRelationship(request.params.relationshipId, input.expectedVersionNo);
1240
+ publishCollaborativeChange(String(relationship.workId), modulePageKey("relationships"));
1151
1241
  noContent(response);
1152
1242
  });
1153
1243
  app.get("/api/entity-versions/:entityType/:entityId", (request, response) => {
@@ -1513,18 +1603,28 @@ export function createRuntime(options) {
1513
1603
  response.type("text/html").send(html);
1514
1604
  };
1515
1605
  app.get(["/", "/index.html"], sendIndexHtml);
1606
+ const setStaticCacheControl = (response) => {
1607
+ const version = response.req.query.v;
1608
+ response.setHeader("Cache-Control", typeof version === "string" && version.length > 0
1609
+ ? "public, max-age=31536000, immutable"
1610
+ : "public, max-age=3600, must-revalidate");
1611
+ };
1516
1612
  const vditorPath = join(process.cwd(), "node_modules", "vditor", "dist");
1517
1613
  if (existsSync(vditorPath)) {
1518
1614
  app.use("/vendor/vditor/dist", express.static(vditorPath, {
1615
+ cacheControl: false,
1519
1616
  index: false,
1520
- maxAge: 0,
1521
- setHeaders: (response) => response.setHeader("Cache-Control", "no-store")
1617
+ etag: true,
1618
+ lastModified: true,
1619
+ setHeaders: setStaticCacheControl
1522
1620
  }));
1523
1621
  }
1524
1622
  app.use(express.static(publicPath, {
1623
+ cacheControl: false,
1525
1624
  index: false,
1526
- maxAge: 0,
1527
- setHeaders: (response) => response.setHeader("Cache-Control", "no-store")
1625
+ etag: true,
1626
+ lastModified: true,
1627
+ setHeaders: setStaticCacheControl
1528
1628
  }));
1529
1629
  app.get("/{*path}", (request, response, next) => {
1530
1630
  if (request.path.startsWith("/api/"))