Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/metadata-watch-event-canonical-enum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@objectstack/spec": major
---

`MetadataWatchEvent.type` now carries only the values the runtime emits: the enum narrows FROM `'add' | 'change' | 'unlink' | 'added' | 'changed' | 'deleted'` TO `'added' | 'changed' | 'deleted'` (#4536, follow-up to #4411).

The three raw chokidar values had zero producers: both emit sites normalize before constructing the event — `packages/metadata/src/node-metadata-manager.ts` translates chokidar's `add`/`change`/`unlink` in the watcher callbacks (`handleFileEvent` accepts only the canonical three), and `packages/metadata/src/metadata-manager.ts` normalizes repository ops (`create`/`update`/`delete` → `added`/`changed`/`deleted`). Consumers parsing events therefore never received the raw values, and no runtime behavior changes.

- FROM: an external implementor could construct events typed `'add'`/`'change'`/`'unlink'` and readers had to (needlessly) branch on six values.
- TO: an implementor constructing events with the raw values must emit `added`/`changed`/`deleted` instead; readers may delete any branches on `add`/`change`/`unlink` — they were unreachable.

No tombstone / ADR-0087 conversion: this is a runtime event envelope type, not authorable metadata — nothing parses it on a load path (the #4411 route).
2 changes: 1 addition & 1 deletion content/docs/references/system/metadata-persistence.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ const result = MetadataCollectionInfo.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'add' \| 'change' \| 'unlink' \| 'added' \| 'changed' \| 'deleted'>` | ✅ | |
| **type** | `Enum<'added' \| 'changed' \| 'deleted'>` | ✅ | |
| **path** | `string` | ✅ | |
| **name** | `string` | optional | |
| **stats** | `{ path?: string; size?: number; mtime?: string; hash?: string; … }` | optional | |
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/src/contracts/metadata-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
*/

import type { MetadataQuery, MetadataQueryResult, MetadataValidationResult, MetadataBulkResult, MetadataDependency } from '../kernel/metadata-plugin.zod';
// The PERSISTENCE-side watch event (`add`/`added`/`changed`/`deleted`/…, path +
// The PERSISTENCE-side watch event (`added`/`changed`/`deleted`, path +
// file stats) — what `MetadataManager.subscribe` relays, as opposed to the
// registration-level events `watch` forwards (`MetadataWatchCallback` below).
// Spec used to carry a second, differently-shaped `MetadataWatchEvent` on
Expand Down Expand Up @@ -423,7 +423,7 @@ export interface IMetadataService {
* NOT {@link watch} with a different return shape: the two carry different
* events. `watch` reports registration-level transitions
* (`registered`/`updated`/`unregistered`); `subscribe` relays the loader
* pipeline's {@link MetadataWatchEvent} (`add`/`changed`/`deleted`, with
* pipeline's {@link MetadataWatchEvent} (`added`/`changed`/`deleted`, with
* path and file stats) — the granularity ObjectQLPlugin's metadata bridge
* re-syncs runtime-authored hooks/actions from. (The first draft of this
* member reused `watch`'s callback type; `MetadataManager implements
Expand Down
18 changes: 14 additions & 4 deletions packages/spec/src/system/metadata-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,16 +381,26 @@ describe('MetadataSaveResultSchema', () => {
});

describe('MetadataWatchEventSchema', () => {
it('should accept valid event types', () => {
const types = ['add', 'change', 'unlink', 'added', 'changed', 'deleted'];
it('should accept the canonical event types', () => {
const types = ['added', 'changed', 'deleted'];
types.forEach((type) => {
expect(() => MetadataWatchEventSchema.parse({ type, path: '/test' })).not.toThrow();
});
});

// Pin (#4536): the raw chokidar vocabulary is translated in
// NodeMetadataManager's watcher callbacks and never reaches the event
// surface — the schema must reject it, not smuggle it back in.
it('should reject the raw chokidar event types', () => {
const rawTypes = ['add', 'change', 'unlink'];
rawTypes.forEach((type) => {
expect(() => MetadataWatchEventSchema.parse({ type, path: '/test' })).toThrow();
});
});

it('should accept full event', () => {
const event = MetadataWatchEventSchema.parse({
type: 'change',
type: 'changed',
path: '/metadata/view.json',
name: 'account_view',
stats: { size: 512 },
Expand All @@ -408,7 +418,7 @@ describe('MetadataWatchEventSchema', () => {
});

it('should reject missing path', () => {
expect(() => MetadataWatchEventSchema.parse({ type: 'add' })).toThrow();
expect(() => MetadataWatchEventSchema.parse({ type: 'added' })).toThrow();
});
});

Expand Down
8 changes: 7 additions & 1 deletion packages/spec/src/system/metadata-persistence.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,15 @@ export const MetadataSaveResultSchema = lazySchema(() => z.object({

/**
* Metadata Watch Event
*
* `type` carries only the values the runtime emits. The raw chokidar
* vocabulary (`add`/`change`/`unlink`) is translated in NodeMetadataManager's
* watcher callbacks (`packages/metadata/src/node-metadata-manager.ts`
* `handleFileEvent`) and never reaches the event surface — the raw values had
* zero producers when they were declared here (#4536, follow-up to #4411).
*/
export const MetadataWatchEventSchema = lazySchema(() => z.object({
type: z.enum(['add', 'change', 'unlink', 'added', 'changed', 'deleted']),
type: z.enum(['added', 'changed', 'deleted']),
path: z.string(),
name: z.string().optional(),
stats: MetadataStatsSchema.optional(),
Expand Down
Loading