@ebbjs/storage 0.3.0 → 1.1.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.
@@ -122,9 +122,10 @@ interface MembershipLink {
122
122
  * handled by the DirtyTracker.
123
123
  *
124
124
  * ## Merge Semantics
125
- * - **put** — full entity replacement
126
- * - **patch** — field-level Last-Writer-Wins (LWW): higher HLC wins; tiebreak by lexicographic `update_id`
127
- * - **delete** — soft delete (sets `deleted_hlc`); patch-on-deleted is ignored
125
+ * - **merge** — Last-Writer-Wins (LWW) at the component's field: higher HLC wins; tiebreak by lexicographic `update_id`
126
+ * - **replace** — the component's field is set whole; the first write to an absent row creates it
127
+ * - **drop** — the component's field is removed; the row survives
128
+ * - **delete** — soft delete (sets `deleted_hlc`); the row keeps its type
128
129
  *
129
130
  * A field value is either a leaf merged by the LWW rule above, or a
130
131
  * `{ map }` of nested field values merged key by key under the same
@@ -136,6 +137,9 @@ interface MembershipLink {
136
137
  * - `get(id)` — returns entity by ID, materializes if dirty
137
138
  * - `set(entity)` — directly sets an entity (bypasses materialization)
138
139
  * - `query(type)` — returns all materialized entities of a type
140
+ * - `queryByComponent(component)` — returns the live ids carrying that
141
+ * `<model>/<field>` component in ascending order, materializing dirty rows
142
+ * of the component's model first
139
143
  * - `queryByRelationship({ as, type, targetId })` — returns the distinct
140
144
  * source ids indexed under that triple in ascending order,
141
145
  * materializing dirty relationship rows first
@@ -154,6 +158,18 @@ interface EntityStore {
154
158
  get(id: string): Promise<Entity | null>;
155
159
  set(entity: Entity): Promise<void>;
156
160
  query(type: string): Promise<readonly Entity[]>;
161
+ /**
162
+ * Live rows currently carrying `component` (`<model>/<field>`), ascending by
163
+ * id. Existence is the set of present fields on a row, so this is an index
164
+ * read rather than a row filter; a tombstoned row's components are not
165
+ * existence.
166
+ *
167
+ * The dirty rows materialized first are the component prefix's model. That is
168
+ * the current `<model>/<field>` addressing rule (#797); a component namespace
169
+ * that is not the row's model (#805's `ebb/*` components on ordinary rows)
170
+ * has to decode the model differently and revisit this step.
171
+ */
172
+ queryByComponent(component: string): Promise<readonly string[]>;
157
173
  queryByRelationship(query: RelationshipIndexQuery): Promise<readonly string[]>;
158
174
  queryByRelationshipSource(query: RelationshipSourceQuery): Promise<readonly RelationshipLink[]>;
159
175
  queryByMembership(groupId: string): Promise<readonly string[]>;
@@ -229,44 +245,51 @@ interface CursorStore {
229
245
  * `conflicted` is a terminal-until-resolved hold: a catch-up pass
230
246
  * classified the Action as a loser, so it is structurally excluded from
231
247
  * `pending()` and never submitted. The application resolves it by
232
- * retrying (back to `pending`) or discarding (removed).
248
+ * sending the Action unchanged (back to `pending`) or clearing it
249
+ * (removed).
233
250
  */
234
251
  type OutboxStatus = "pending" | "acknowledged" | "error" | "conflicted";
235
252
  /**
236
253
  * One side of a contested slot: the buffered local write, or the value
237
254
  * the server holds once the catch-up window is applied. A leaf carries
238
- * its LWW triple; a whole-field map replacement has no single leaf, so
255
+ * its LWW triple; a whole-component map replacement has no single leaf, so
239
256
  * `update_id` / `hlc` are absent and `value` is the projected map. A
240
- * field an inbound `put` or `delete` reverted also has no triple, and
257
+ * component an inbound `merge` or `delete` reverted also has no triple, and
241
258
  * `value` is `undefined` because the server holds no value.
242
259
  */
243
260
  interface ConflictSide {
244
261
  /** Projected leaf or map value. `undefined` when the server holds nothing. */
245
262
  readonly value: unknown;
246
- /** `update_id` of the FieldValue; absent for a map side or a reverted field. */
263
+ /** `update_id` of the FieldValue; absent for a map side or a reverted component. */
247
264
  readonly update_id?: string;
248
- /** HLC carried by the FieldValue; absent for a map side or a reverted field. */
265
+ /** HLC carried by the FieldValue; absent for a map side or a reverted component. */
249
266
  readonly hlc?: string;
250
267
  }
251
268
  /**
252
- * One contested position on a losing Action: a field plus the map-key
253
- * path from the field root to the position. `path` is empty for a plain
254
- * leaf field, an entity-level touch that overlaps no position, or a
255
- * field an inbound `put` / `delete` reverted. Concurrent writes to
269
+ * One contested position on a losing Action: a component plus the map-key
270
+ * path from the component's value root to the position. `path` is empty for a
271
+ * plain leaf component, an entity-level touch that overlaps no position, or a
272
+ * component an inbound `merge` / `delete` reverted. Concurrent writes to
256
273
  * different keys are different slots and so are not conflicts.
257
274
  */
258
275
  interface ConflictSlot {
259
276
  /** The `subject_id` of the Update that carried the write. */
260
277
  readonly subjectId: string;
261
- /** The field name on that subject. */
262
- readonly field: string;
263
- /** Ordered map keys from the field root to the position; empty at the root. */
278
+ /** The `<model>/<field>` component name on that subject. */
279
+ readonly component: string;
280
+ /** Ordered map keys from the component root to the position; empty at the root. */
264
281
  readonly path: readonly string[];
265
282
  /** The buffered local value at the position. */
266
283
  readonly local: ConflictSide;
267
284
  /** The value the server holds at the position after the missed window. */
268
285
  readonly remote: ConflictSide;
269
286
  }
287
+ /** One missed Action that out-dated a buffered write and made it a conflict. */
288
+ interface ConflictRemoteAction {
289
+ readonly actorId: string;
290
+ readonly actionId: string;
291
+ readonly gsn: number;
292
+ }
270
293
  /**
271
294
  * The before/after entity rows for one losing subject. `before` is the
272
295
  * row as the client had it immediately before the catch-up window was
@@ -278,7 +301,8 @@ interface ConflictSlot {
278
301
  */
279
302
  interface ConflictSubjectState {
280
303
  readonly subjectId: string;
281
- readonly subjectType: string;
304
+ /** The model the row is committed under. */
305
+ readonly model: string;
282
306
  readonly before: Entity | null;
283
307
  readonly after: Entity | null;
284
308
  }
@@ -305,6 +329,18 @@ interface OutboxConflict {
305
329
  * application reads that as "nothing of mine would be lost".
306
330
  */
307
331
  readonly slots: readonly ConflictSlot[];
332
+ /**
333
+ * The looser of the losing subjects' declared scopes: `"entity"` when
334
+ * any losing subject declared entity scope, else `"field"`. The union is
335
+ * spelled inline so storage stays independent of the schema package.
336
+ */
337
+ readonly scope: "field" | "component" | "entity";
338
+ /**
339
+ * The concurrent missed Actions that touched the losing subjects,
340
+ * deduped by action id, in window order. The remote half of the
341
+ * report an application builds a policy from.
342
+ */
343
+ readonly remoteActions: readonly ConflictRemoteAction[];
308
344
  /** The catch-up pass that produced this entry. */
309
345
  readonly catchUpId: string;
310
346
  /**
@@ -353,10 +389,9 @@ interface OutboxEntry {
353
389
  interface OutboxStore {
354
390
  put(entry: OutboxEntry): Promise<void>;
355
391
  /**
356
- * Persist `entry` and delete `deleteId` in one durable operation. Used
357
- * to resolve a conflict by retrying: the fresh re-assert must never
358
- * exist without the conflicted row being gone, nor the row be gone
359
- * without the re-assert persisted.
392
+ * Persist `entry` and delete `deleteId` in one durable operation: the
393
+ * replacement must never exist without the replaced row being gone,
394
+ * nor the row be gone without the replacement persisted.
360
395
  */
361
396
  putAndDelete(entry: OutboxEntry, deleteId: string): Promise<void>;
362
397
  list(): Promise<readonly OutboxEntry[]>;