authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-28 22:54:37-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-28 22:54:37-07:00
loga1c428b6e266434e33b43d2e40d81d6851c1bb3c
treeb26fdd2b948bfcdbf42ac2813476db19bbf5a0d6
parent00f3efb95b485a40380af8295a5119b03095858b
signature Signed by PGP key B5690EEEBB952194

Refactor projection pipeline side effects (#1512)


2 files changed, 242 insertions(+), 229 deletions(-)

apps/server/src/orchestration/Layers/ProjectionPipeline.ts+234-221
......@@ -77,7 +77,7 @@ interface AttachmentSideEffects {
7777 readonly prunedThreadRelativePaths: Map<string, Set<string>>;
7878}
7979
80const materializeAttachmentsForProjection = Effect.fn(
80const materializeAttachmentsForProjection = Effect.fn("materializeAttachmentsForProjection")(
8181 (input: { readonly attachments: ReadonlyArray<ChatAttachment> }) =>
8282 Effect.succeed(input.attachments.length === 0 ? [] : input.attachments),
8383);
......@@ -238,124 +238,145 @@ function collectThreadAttachmentRelativePaths(
238238 return relativePaths;
239239}
240240
241const runAttachmentSideEffects = Effect.fn(function* (sideEffects: AttachmentSideEffects) {
241const runAttachmentSideEffects = Effect.fn("runAttachmentSideEffects")(function* (
242 sideEffects: AttachmentSideEffects,
243) {
242244 const serverConfig = yield* Effect.service(ServerConfig);
243245 const fileSystem = yield* Effect.service(FileSystem.FileSystem);
244246 const path = yield* Effect.service(Path.Path);
245247
246248 const attachmentsRootDir = serverConfig.attachmentsDir;
247
248 yield* Effect.forEach(
249 sideEffects.deletedThreadIds,
250 (threadId) =>
251 Effect.gen(function* () {
252 const threadSegment = toSafeThreadAttachmentSegment(threadId);
253 if (!threadSegment) {
254 yield* Effect.logWarning("skipping attachment cleanup for unsafe thread id", {
255 threadId,
256 });
257 return;
258 }
259 const entries = yield* fileSystem
260 .readDirectory(attachmentsRootDir, { recursive: false })
261 .pipe(Effect.catch(() => Effect.succeed([] as Array<string>)));
262 yield* Effect.forEach(
263 entries,
264 (entry) =>
265 Effect.gen(function* () {
266 const normalizedEntry = entry.replace(/^[/\\]+/, "").replace(/\\/g, "/");
267 if (normalizedEntry.length === 0 || normalizedEntry.includes("/")) {
268 return;
269 }
270 const attachmentId = parseAttachmentIdFromRelativePath(normalizedEntry);
271 if (!attachmentId) {
272 return;
273 }
274 const attachmentThreadSegment = parseThreadSegmentFromAttachmentId(attachmentId);
275 if (!attachmentThreadSegment || attachmentThreadSegment !== threadSegment) {
276 return;
277 }
278 yield* fileSystem.remove(path.join(attachmentsRootDir, normalizedEntry), {
279 force: true,
280 });
281 }),
282 { concurrency: 1 },
283 );
284 }),
285 { concurrency: 1 },
249 const readAttachmentRootEntries = fileSystem
250 .readDirectory(attachmentsRootDir, { recursive: false })
251 .pipe(Effect.catch(() => Effect.succeed([] as Array<string>)));
252
253 const removeDeletedThreadAttachmentEntry = Effect.fn("removeDeletedThreadAttachmentEntry")(
254 function* (threadSegment: string, entry: string) {
255 const normalizedEntry = entry.replace(/^[/\\]+/, "").replace(/\\/g, "/");
256 if (normalizedEntry.length === 0 || normalizedEntry.includes("/")) {
257 return;
258 }
259 const attachmentId = parseAttachmentIdFromRelativePath(normalizedEntry);
260 if (!attachmentId) {
261 return;
262 }
263 const attachmentThreadSegment = parseThreadSegmentFromAttachmentId(attachmentId);
264 if (!attachmentThreadSegment || attachmentThreadSegment !== threadSegment) {
265 return;
266 }
267 yield* fileSystem.remove(path.join(attachmentsRootDir, normalizedEntry), {
268 force: true,
269 });
270 },
286271 );
287272
273 const deleteThreadAttachments = Effect.fn("deleteThreadAttachments")(function* (
274 threadId: string,
275 ) {
276 const threadSegment = toSafeThreadAttachmentSegment(threadId);
277 if (!threadSegment) {
278 yield* Effect.logWarning("skipping attachment cleanup for unsafe thread id", {
279 threadId,
280 });
281 return;
282 }
283
284 const entries = yield* readAttachmentRootEntries;
285 yield* Effect.forEach(
286 entries,
287 (entry) => removeDeletedThreadAttachmentEntry(threadSegment, entry),
288 {
289 concurrency: 1,
290 },
291 );
292 });
293
294 const pruneThreadAttachmentEntry = Effect.fn("pruneThreadAttachmentEntry")(function* (
295 threadSegment: string,
296 keptThreadRelativePaths: Set<string>,
297 entry: string,
298 ) {
299 const relativePath = entry.replace(/^[/\\]+/, "").replace(/\\/g, "/");
300 if (relativePath.length === 0 || relativePath.includes("/")) {
301 return;
302 }
303 const attachmentId = parseAttachmentIdFromRelativePath(relativePath);
304 if (!attachmentId) {
305 return;
306 }
307 const attachmentThreadSegment = parseThreadSegmentFromAttachmentId(attachmentId);
308 if (!attachmentThreadSegment || attachmentThreadSegment !== threadSegment) {
309 return;
310 }
311
312 const absolutePath = path.join(attachmentsRootDir, relativePath);
313 const fileInfo = yield* fileSystem
314 .stat(absolutePath)
315 .pipe(Effect.catch(() => Effect.succeed(null)));
316 if (!fileInfo || fileInfo.type !== "File") {
317 return;
318 }
319
320 if (!keptThreadRelativePaths.has(relativePath)) {
321 yield* fileSystem.remove(absolutePath, { force: true });
322 }
323 });
324
325 const pruneThreadAttachments = Effect.fn("pruneThreadAttachments")(function* (
326 threadId: string,
327 keptThreadRelativePaths: Set<string>,
328 ) {
329 if (sideEffects.deletedThreadIds.has(threadId)) {
330 return;
331 }
332
333 const threadSegment = toSafeThreadAttachmentSegment(threadId);
334 if (!threadSegment) {
335 yield* Effect.logWarning("skipping attachment prune for unsafe thread id", { threadId });
336 return;
337 }
338
339 const entries = yield* readAttachmentRootEntries;
340 yield* Effect.forEach(
341 entries,
342 (entry) => pruneThreadAttachmentEntry(threadSegment, keptThreadRelativePaths, entry),
343 { concurrency: 1 },
344 );
345 });
346
347 yield* Effect.forEach(sideEffects.deletedThreadIds, deleteThreadAttachments, {
348 concurrency: 1,
349 });
350
288351 yield* Effect.forEach(
289352 sideEffects.prunedThreadRelativePaths.entries(),
290 ([threadId, keptThreadRelativePaths]) => {
291 if (sideEffects.deletedThreadIds.has(threadId)) {
292 return Effect.void;
293 }
294 return Effect.gen(function* () {
295 const threadSegment = toSafeThreadAttachmentSegment(threadId);
296 if (!threadSegment) {
297 yield* Effect.logWarning("skipping attachment prune for unsafe thread id", { threadId });
298 return;
299 }
300 const entries = yield* fileSystem
301 .readDirectory(attachmentsRootDir, { recursive: false })
302 .pipe(Effect.catch(() => Effect.succeed([] as Array<string>)));
303 yield* Effect.forEach(
304 entries,
305 (entry) =>
306 Effect.gen(function* () {
307 const relativePath = entry.replace(/^[/\\]+/, "").replace(/\\/g, "/");
308 if (relativePath.length === 0 || relativePath.includes("/")) {
309 return;
310 }
311 const attachmentId = parseAttachmentIdFromRelativePath(relativePath);
312 if (!attachmentId) {
313 return;
314 }
315 const attachmentThreadSegment = parseThreadSegmentFromAttachmentId(attachmentId);
316 if (!attachmentThreadSegment || attachmentThreadSegment !== threadSegment) {
317 return;
318 }
319
320 const absolutePath = path.join(attachmentsRootDir, relativePath);
321 const fileInfo = yield* fileSystem
322 .stat(absolutePath)
323 .pipe(Effect.catch(() => Effect.succeed(null)));
324 if (!fileInfo || fileInfo.type !== "File") {
325 return;
326 }
327
328 if (!keptThreadRelativePaths.has(relativePath)) {
329 yield* fileSystem.remove(absolutePath, { force: true });
330 }
331 }),
332 { concurrency: 1 },
333 );
334 });
335 },
353 ([threadId, keptThreadRelativePaths]) =>
354 pruneThreadAttachments(threadId, keptThreadRelativePaths),
336355 { concurrency: 1 },
337356 );
338357});
339358
340const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
341 const sql = yield* SqlClient.SqlClient;
342 const eventStore = yield* OrchestrationEventStore;
343 const projectionStateRepository = yield* ProjectionStateRepository;
344 const projectionProjectRepository = yield* ProjectionProjectRepository;
345 const projectionThreadRepository = yield* ProjectionThreadRepository;
346 const projectionThreadMessageRepository = yield* ProjectionThreadMessageRepository;
347 const projectionThreadProposedPlanRepository = yield* ProjectionThreadProposedPlanRepository;
348 const projectionThreadActivityRepository = yield* ProjectionThreadActivityRepository;
349 const projectionThreadSessionRepository = yield* ProjectionThreadSessionRepository;
350 const projectionTurnRepository = yield* ProjectionTurnRepository;
351 const projectionPendingApprovalRepository = yield* ProjectionPendingApprovalRepository;
352
353 const fileSystem = yield* FileSystem.FileSystem;
354 const path = yield* Path.Path;
355 const serverConfig = yield* ServerConfig;
356
357 const applyProjectsProjection: ProjectorDefinition["apply"] = (event, _attachmentSideEffects) =>
358 Effect.gen(function* () {
359const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjectionPipeline")(
360 function* () {
361 const sql = yield* SqlClient.SqlClient;
362 const eventStore = yield* OrchestrationEventStore;
363 const projectionStateRepository = yield* ProjectionStateRepository;
364 const projectionProjectRepository = yield* ProjectionProjectRepository;
365 const projectionThreadRepository = yield* ProjectionThreadRepository;
366 const projectionThreadMessageRepository = yield* ProjectionThreadMessageRepository;
367 const projectionThreadProposedPlanRepository = yield* ProjectionThreadProposedPlanRepository;
368 const projectionThreadActivityRepository = yield* ProjectionThreadActivityRepository;
369 const projectionThreadSessionRepository = yield* ProjectionThreadSessionRepository;
370 const projectionTurnRepository = yield* ProjectionTurnRepository;
371 const projectionPendingApprovalRepository = yield* ProjectionPendingApprovalRepository;
372
373 const fileSystem = yield* FileSystem.FileSystem;
374 const path = yield* Path.Path;
375 const serverConfig = yield* ServerConfig;
376
377 const applyProjectsProjection: ProjectorDefinition["apply"] = Effect.fn(
378 "applyProjectsProjection",
379 )(function* (event, _attachmentSideEffects) {
359380 switch (event.type) {
360381 case "project.created":
361382 yield* projectionProjectRepository.upsert({
......@@ -412,8 +433,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
412433 }
413434 });
414435
415 const applyThreadsProjection: ProjectorDefinition["apply"] = (event, attachmentSideEffects) =>
416 Effect.gen(function* () {
436 const applyThreadsProjection: ProjectorDefinition["apply"] = Effect.fn(
437 "applyThreadsProjection",
438 )(function* (event, attachmentSideEffects) {
417439 switch (event.type) {
418440 case "thread.created":
419441 yield* projectionThreadRepository.upsert({
......@@ -597,11 +619,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
597619 }
598620 });
599621
600 const applyThreadMessagesProjection: ProjectorDefinition["apply"] = (
601 event,
602 attachmentSideEffects,
603 ) =>
604 Effect.gen(function* () {
622 const applyThreadMessagesProjection: ProjectorDefinition["apply"] = Effect.fn(
623 "applyThreadMessagesProjection",
624 )(function* (event, attachmentSideEffects) {
605625 switch (event.type) {
606626 case "thread.message-sent": {
607627 const existingRows = yield* projectionThreadMessageRepository.listByThreadId({
......@@ -674,11 +694,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
674694 }
675695 });
676696
677 const applyThreadProposedPlansProjection: ProjectorDefinition["apply"] = (
678 event,
679 _attachmentSideEffects,
680 ) =>
681 Effect.gen(function* () {
697 const applyThreadProposedPlansProjection: ProjectorDefinition["apply"] = Effect.fn(
698 "applyThreadProposedPlansProjection",
699 )(function* (event, _attachmentSideEffects) {
682700 switch (event.type) {
683701 case "thread.proposed-plan-upserted":
684702 yield* projectionThreadProposedPlanRepository.upsert({
......@@ -727,11 +745,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
727745 }
728746 });
729747
730 const applyThreadActivitiesProjection: ProjectorDefinition["apply"] = (
731 event,
732 _attachmentSideEffects,
733 ) =>
734 Effect.gen(function* () {
748 const applyThreadActivitiesProjection: ProjectorDefinition["apply"] = Effect.fn(
749 "applyThreadActivitiesProjection",
750 )(function* (event, _attachmentSideEffects) {
735751 switch (event.type) {
736752 case "thread.activity-appended":
737753 yield* projectionThreadActivityRepository.upsert({
......@@ -781,11 +797,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
781797 }
782798 });
783799
784 const applyThreadSessionsProjection: ProjectorDefinition["apply"] = (
785 event,
786 _attachmentSideEffects,
787 ) =>
788 Effect.gen(function* () {
800 const applyThreadSessionsProjection: ProjectorDefinition["apply"] = Effect.fn(
801 "applyThreadSessionsProjection",
802 )(function* (event, _attachmentSideEffects) {
789803 if (event.type !== "thread.session-set") {
790804 return;
791805 }
......@@ -800,11 +814,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
800814 });
801815 });
802816
803 const applyThreadTurnsProjection: ProjectorDefinition["apply"] = (
804 event,
805 _attachmentSideEffects,
806 ) =>
807 Effect.gen(function* () {
817 const applyThreadTurnsProjection: ProjectorDefinition["apply"] = Effect.fn(
818 "applyThreadTurnsProjection",
819 )(function* (event, _attachmentSideEffects) {
808820 switch (event.type) {
809821 case "thread.turn-start-requested": {
810822 yield* projectionTurnRepository.replacePendingTurnStart({
......@@ -1058,13 +1070,11 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
10581070 }
10591071 });
10601072
1061 const applyCheckpointsProjection: ProjectorDefinition["apply"] = () => Effect.void;
1073 const applyCheckpointsProjection: ProjectorDefinition["apply"] = () => Effect.void;
10621074
1063 const applyPendingApprovalsProjection: ProjectorDefinition["apply"] = (
1064 event,
1065 _attachmentSideEffects,
1066 ) =>
1067 Effect.gen(function* () {
1075 const applyPendingApprovalsProjection: ProjectorDefinition["apply"] = Effect.fn(
1076 "applyPendingApprovalsProjection",
1077 )(function* (event, _attachmentSideEffects) {
10681078 switch (event.type) {
10691079 case "thread.activity-appended": {
10701080 const requestId =
......@@ -1150,47 +1160,49 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
11501160 }
11511161 });
11521162
1153 const projectors: ReadonlyArray<ProjectorDefinition> = [
1154 {
1155 name: ORCHESTRATION_PROJECTOR_NAMES.projects,
1156 apply: applyProjectsProjection,
1157 },
1158 {
1159 name: ORCHESTRATION_PROJECTOR_NAMES.threadMessages,
1160 apply: applyThreadMessagesProjection,
1161 },
1162 {
1163 name: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans,
1164 apply: applyThreadProposedPlansProjection,
1165 },
1166 {
1167 name: ORCHESTRATION_PROJECTOR_NAMES.threadActivities,
1168 apply: applyThreadActivitiesProjection,
1169 },
1170 {
1171 name: ORCHESTRATION_PROJECTOR_NAMES.threadSessions,
1172 apply: applyThreadSessionsProjection,
1173 },
1174 {
1175 name: ORCHESTRATION_PROJECTOR_NAMES.threadTurns,
1176 apply: applyThreadTurnsProjection,
1177 },
1178 {
1179 name: ORCHESTRATION_PROJECTOR_NAMES.checkpoints,
1180 apply: applyCheckpointsProjection,
1181 },
1182 {
1183 name: ORCHESTRATION_PROJECTOR_NAMES.pendingApprovals,
1184 apply: applyPendingApprovalsProjection,
1185 },
1186 {
1187 name: ORCHESTRATION_PROJECTOR_NAMES.threads,
1188 apply: applyThreadsProjection,
1189 },
1190 ];
1191
1192 const runProjectorForEvent = (projector: ProjectorDefinition, event: OrchestrationEvent) =>
1193 Effect.gen(function* () {
1163 const projectors: ReadonlyArray<ProjectorDefinition> = [
1164 {
1165 name: ORCHESTRATION_PROJECTOR_NAMES.projects,
1166 apply: applyProjectsProjection,
1167 },
1168 {
1169 name: ORCHESTRATION_PROJECTOR_NAMES.threadMessages,
1170 apply: applyThreadMessagesProjection,
1171 },
1172 {
1173 name: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans,
1174 apply: applyThreadProposedPlansProjection,
1175 },
1176 {
1177 name: ORCHESTRATION_PROJECTOR_NAMES.threadActivities,
1178 apply: applyThreadActivitiesProjection,
1179 },
1180 {
1181 name: ORCHESTRATION_PROJECTOR_NAMES.threadSessions,
1182 apply: applyThreadSessionsProjection,
1183 },
1184 {
1185 name: ORCHESTRATION_PROJECTOR_NAMES.threadTurns,
1186 apply: applyThreadTurnsProjection,
1187 },
1188 {
1189 name: ORCHESTRATION_PROJECTOR_NAMES.checkpoints,
1190 apply: applyCheckpointsProjection,
1191 },
1192 {
1193 name: ORCHESTRATION_PROJECTOR_NAMES.pendingApprovals,
1194 apply: applyPendingApprovalsProjection,
1195 },
1196 {
1197 name: ORCHESTRATION_PROJECTOR_NAMES.threads,
1198 apply: applyThreadsProjection,
1199 },
1200 ];
1201
1202 const runProjectorForEvent = Effect.fn("runProjectorForEvent")(function* (
1203 projector: ProjectorDefinition,
1204 event: OrchestrationEvent,
1205 ) {
11941206 const attachmentSideEffects: AttachmentSideEffects = {
11951207 deletedThreadIds: new Set<string>(),
11961208 prunedThreadRelativePaths: new Map<string, Set<string>>(),
......@@ -1220,63 +1232,64 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
12201232 );
12211233 });
12221234
1223 const bootstrapProjector = (projector: ProjectorDefinition) =>
1224 projectionStateRepository
1225 .getByProjector({
1226 projector: projector.name,
1227 })
1228 .pipe(
1229 Effect.flatMap((stateRow) =>
1230 Stream.runForEach(
1231 eventStore.readFromSequence(
1232 Option.isSome(stateRow) ? stateRow.value.lastAppliedSequence : 0,
1235 const bootstrapProjector = (projector: ProjectorDefinition) =>
1236 projectionStateRepository
1237 .getByProjector({
1238 projector: projector.name,
1239 })
1240 .pipe(
1241 Effect.flatMap((stateRow) =>
1242 Stream.runForEach(
1243 eventStore.readFromSequence(
1244 Option.isSome(stateRow) ? stateRow.value.lastAppliedSequence : 0,
1245 ),
1246 (event) => runProjectorForEvent(projector, event),
12331247 ),
1234 (event) => runProjectorForEvent(projector, event),
12351248 ),
1249 );
1250
1251 const projectEvent: OrchestrationProjectionPipelineShape["projectEvent"] = (event) =>
1252 Effect.forEach(projectors, (projector) => runProjectorForEvent(projector, event), {
1253 concurrency: 1,
1254 }).pipe(
1255 Effect.provideService(FileSystem.FileSystem, fileSystem),
1256 Effect.provideService(Path.Path, path),
1257 Effect.provideService(ServerConfig, serverConfig),
1258 Effect.asVoid,
1259 Effect.catchTag("SqlError", (sqlError) =>
1260 Effect.fail(toPersistenceSqlError("ProjectionPipeline.projectEvent:query")(sqlError)),
12361261 ),
12371262 );
12381263
1239 const projectEvent: OrchestrationProjectionPipelineShape["projectEvent"] = (event) =>
1240 Effect.forEach(projectors, (projector) => runProjectorForEvent(projector, event), {
1241 concurrency: 1,
1242 }).pipe(
1264 const bootstrap: OrchestrationProjectionPipelineShape["bootstrap"] = Effect.forEach(
1265 projectors,
1266 bootstrapProjector,
1267 { concurrency: 1 },
1268 ).pipe(
12431269 Effect.provideService(FileSystem.FileSystem, fileSystem),
12441270 Effect.provideService(Path.Path, path),
12451271 Effect.provideService(ServerConfig, serverConfig),
12461272 Effect.asVoid,
1273 Effect.tap(() =>
1274 Effect.log("orchestration projection pipeline bootstrapped").pipe(
1275 Effect.annotateLogs({ projectors: projectors.length }),
1276 ),
1277 ),
12471278 Effect.catchTag("SqlError", (sqlError) =>
1248 Effect.fail(toPersistenceSqlError("ProjectionPipeline.projectEvent:query")(sqlError)),
1279 Effect.fail(toPersistenceSqlError("ProjectionPipeline.bootstrap:query")(sqlError)),
12491280 ),
12501281 );
12511282
1252 const bootstrap: OrchestrationProjectionPipelineShape["bootstrap"] = Effect.forEach(
1253 projectors,
1254 bootstrapProjector,
1255 { concurrency: 1 },
1256 ).pipe(
1257 Effect.provideService(FileSystem.FileSystem, fileSystem),
1258 Effect.provideService(Path.Path, path),
1259 Effect.provideService(ServerConfig, serverConfig),
1260 Effect.asVoid,
1261 Effect.tap(() =>
1262 Effect.log("orchestration projection pipeline bootstrapped").pipe(
1263 Effect.annotateLogs({ projectors: projectors.length }),
1264 ),
1265 ),
1266 Effect.catchTag("SqlError", (sqlError) =>
1267 Effect.fail(toPersistenceSqlError("ProjectionPipeline.bootstrap:query")(sqlError)),
1268 ),
1269 );
1270
1271 return {
1272 bootstrap,
1273 projectEvent,
1274 } satisfies OrchestrationProjectionPipelineShape;
1275});
1283 return {
1284 bootstrap,
1285 projectEvent,
1286 } satisfies OrchestrationProjectionPipelineShape;
1287 },
1288);
12761289
12771290export const OrchestrationProjectionPipelineLive = Layer.effect(
12781291 OrchestrationProjectionPipeline,
1279 makeOrchestrationProjectionPipeline,
1292 makeOrchestrationProjectionPipeline(),
12801293).pipe(
12811294 Layer.provideMerge(NodeServices.layer),
12821295 Layer.provideMerge(ProjectionProjectRepositoryLive),
docs/effect-fn-checklist.md+8-8
......@@ -47,7 +47,7 @@ Effect.fn("name")(
4747- [x] `apps/server/src/git/Layers/GitCore.ts`
4848- [x] `apps/server/src/git/Layers/GitManager.ts`
4949- [ ] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`
50- [ ] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`
50- [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts`
5151- [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts`
5252- [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts`
5353- [ ] `Everything else`
......@@ -90,13 +90,13 @@ Effect.fn("name")(
9090
9191### `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` (`25`)
9292
93- [ ] [runProjectorForEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161)
94- [ ] [applyProjectsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357)
95- [ ] [applyThreadsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415)
96- [ ] `Effect.forEach(..., threadId => Effect.gen(...))` callbacks around `L250`
97- [ ] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L264`
98- [ ] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305`
99- [ ] Remaining apply helpers in this file
93- [x] [runProjectorForEvent](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161)
94- [x] [applyProjectsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357)
95- [x] [applyThreadsProjection](/Users/julius/Development/Work/codething-mvp/apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415)
96- [x] `Effect.forEach(..., threadId => Effect.gen(...))` callbacks around `L250`
97- [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L264`
98- [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305`
99- [x] Remaining apply helpers in this file
100100
101101### `apps/server/src/provider/Layers/ProviderService.ts` (`24`)
102102