@verdant-web/store 4.1.6 → 4.3.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.
Files changed (46) hide show
  1. package/dist/bundle/index.js +9 -7
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/esm/UndoHistory.d.ts +2 -0
  4. package/dist/esm/UndoHistory.js +6 -0
  5. package/dist/esm/UndoHistory.js.map +1 -1
  6. package/dist/esm/__tests__/entities.test.js +143 -5
  7. package/dist/esm/__tests__/entities.test.js.map +1 -1
  8. package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -0
  9. package/dist/esm/__tests__/fixtures/testStorage.js +1 -1
  10. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  11. package/dist/esm/client/Client.js +1 -0
  12. package/dist/esm/client/Client.js.map +1 -1
  13. package/dist/esm/client/ClientDescriptor.js +4 -1
  14. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  15. package/dist/esm/context/context.d.ts +6 -0
  16. package/dist/esm/entities/Entity.d.ts +22 -2
  17. package/dist/esm/entities/Entity.js +127 -5
  18. package/dist/esm/entities/Entity.js.map +1 -1
  19. package/dist/esm/entities/EntityCache.d.ts +1 -1
  20. package/dist/esm/entities/EntityMetadata.d.ts +2 -2
  21. package/dist/esm/entities/EntityMetadata.js.map +1 -1
  22. package/dist/esm/entities/EntityStore.d.ts +4 -4
  23. package/dist/esm/entities/EntityStore.js +4 -9
  24. package/dist/esm/entities/EntityStore.js.map +1 -1
  25. package/dist/esm/entities/OperationBatcher.d.ts +1 -1
  26. package/dist/esm/entities/OperationBatcher.js +2 -0
  27. package/dist/esm/entities/OperationBatcher.js.map +1 -1
  28. package/dist/esm/index.d.ts +16 -19
  29. package/dist/esm/index.js +10 -12
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/persistence/migration/engine.d.ts +1 -1
  32. package/dist/esm/persistence/migration/engine.js +7 -0
  33. package/dist/esm/persistence/migration/engine.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/UndoHistory.ts +7 -0
  36. package/src/__tests__/entities.test.ts +160 -5
  37. package/src/__tests__/fixtures/testStorage.ts +1 -1
  38. package/src/client/Client.ts +1 -0
  39. package/src/client/ClientDescriptor.ts +8 -0
  40. package/src/context/context.ts +7 -0
  41. package/src/entities/Entity.ts +151 -5
  42. package/src/entities/EntityMetadata.ts +5 -4
  43. package/src/entities/EntityStore.ts +10 -11
  44. package/src/entities/OperationBatcher.ts +16 -2
  45. package/src/index.ts +39 -39
  46. package/src/persistence/migration/engine.ts +9 -2
package/dist/esm/index.js CHANGED
@@ -1,17 +1,15 @@
1
- import { ClientDescriptor, } from './client/ClientDescriptor.js';
2
1
  import { Client } from './client/Client.js';
3
- export { ClientDescriptor };
4
- export { Client };
2
+ import { ClientDescriptor, } from './client/ClientDescriptor.js';
3
+ export { Client, ClientDescriptor };
5
4
  // backward compat
6
- export { ClientDescriptor as StorageDescriptor };
7
- export { Client as Storage };
8
- export { Entity } from './entities/Entity.js';
9
- export { ServerSync } from './sync/Sync.js';
10
- export { EntityFile } from './files/EntityFile.js';
11
- export { schema, createMigration } from '@verdant-web/common';
12
- export * from './utils/id.js';
13
- export { UndoHistory } from './UndoHistory.js';
5
+ export { createMigration, schema } from '@verdant-web/common';
14
6
  export * from './authorization.js';
15
- export * from './sync/cliSync.js';
7
+ export { Entity, getEntityClient } from './entities/Entity.js';
8
+ export { EntityFile } from './files/EntityFile.js';
16
9
  export { IdbPersistence } from './persistence/idb/idbPersistence.js';
10
+ export * from './sync/cliSync.js';
11
+ export { ServerSync } from './sync/Sync.js';
12
+ export { UndoHistory } from './UndoHistory.js';
13
+ export * from './utils/id.js';
14
+ export { Client as Storage, ClientDescriptor as StorageDescriptor };
17
15
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,gBAAgB,GAEhB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,kBAAkB;AAClB,OAAO,EAAE,gBAAgB,IAAI,iBAAiB,EAAE,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,CAAC;AAG7B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAU9C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,UAAU,EAA2B,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AA4B9D,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EACN,gBAAgB,GAEhB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;AACpC,kBAAkB;AAClB,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAyB9D,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAU/D,OAAO,EAAE,UAAU,EAA2B,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAKrE,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,gBAAgB,IAAI,iBAAiB,EAAE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { Migration, MigrationEngine } from '@verdant-web/common';
2
- import { OpenDocumentDbContext } from './types.js';
3
2
  import { PersistenceNamespace } from '../interfaces.js';
3
+ import { OpenDocumentDbContext } from './types.js';
4
4
  export declare function getMigrationEngine({ migration, context, ns, }: {
5
5
  log?: (...args: any[]) => void;
6
6
  migration: Migration;
@@ -108,7 +108,14 @@ export async function getMigrationEngine({ migration, context, ns, }) {
108
108
  removeOidPropertiesFromAllSubObjects(newValue);
109
109
  assignOidsToAllSubObjects(newValue);
110
110
  const patches = diffToPatches(original, newValue, () => context.time.zeroWithVersion(migration.version), undefined, [], {
111
+ // incoming unknown objects are assumed to be the same
112
+ // as any pre-existing object.
111
113
  mergeUnknownObjects: true,
114
+ // if a field is undefined in the new value, it should be
115
+ // erased. this is the only way to allow users to remove
116
+ // entries in maps during migrations. it is a little
117
+ // dangerous for other types, though.
118
+ defaultUndefined: false,
112
119
  authz,
113
120
  });
114
121
  if (patches.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../../src/persistence/migration/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,gBAAgB,EAChB,MAAM,EACN,yBAAyB,EACzB,SAAS,EACT,SAAS,EACT,aAAa,EACb,MAAM,EACN,oCAAoC,GAEpC,MAAM,qBAAqB,CAAC;AAI7B,SAAS,qBAAqB,CAAC,EAC9B,SAAS,EACT,OAAO,EACP,GAAG,GAKH;IACA,OAAO,SAAS,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE;QAC9D,GAAG,CAAC,cAAc,CAAC,GAAG;YACrB,GAAG,EAAE,KAAK,EAAE,GAAQ,EAAE,OAAuC,EAAE,EAAE;gBAChE,eAAe;gBACf,gBAAgB,CAAC,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,GAAG,CAAC,CAAC;gBACvE,MAAM,UAAU,GACf,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,CAAC;gBACjE,MAAM,GAAG,GAAG,SAAS,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;gBAClD,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAElB,MAAM,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CACxD,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC;oBACnB,UAAU,EAAE,GAAG,CAAC,YAAY,CAAC,gBAAgB,CAC5C,GAAG,EACH,GAAG,EACH,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,CACf;oBACD,OAAO,EAAE,IAAI;iBACb,CAAC,CACF,CAAC;gBACF,OAAO,GAAG,CAAC;YACZ,CAAC;YACD,MAAM,EAAE,KAAK,EAAE,EAAU,EAAE,EAAE;gBAC5B,MAAM,OAAO,GAAG,SAAS,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBAC9C,MAAM,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CACxD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAChC,CAAC;YACH,CAAC;SACD,CAAC;QACF,OAAO,GAAG,CAAC;IACZ,CAAC,EAAE,EAAS,CAAC,CAAC;AACf,CAAC;AAED,SAAS,mBAAmB,CAAC,EAC5B,SAAS,EACT,OAAO,EACP,SAAS,GAKT;IACA,OAAO,SAAS,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE;QAC9D,GAAG,CAAC,cAAc,CAAC,GAAG;YACrB,GAAG,EAAE,KAAK,EAAE,EAAU,EAAE,EAAE;gBACzB,MAAM,GAAG,GAAG,SAAS,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBAC1C,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE;oBACvD,sFAAsF;oBACtF,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC;iBAC5D,CAAC,CAAC;gBACH,OAAO,GAAG,CAAC;YACZ,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,MAAwB,EAAE,EAAE;gBAC3C,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC;oBACtC,UAAU,EAAE,cAAc;oBAC1B,KAAK,EAAE,MAAM;iBACb,CAAC,CAAC;gBACH,IAAI,CAAC,GAAG;oBAAE,OAAO,IAAI,CAAC;gBACtB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE;oBACvD,sFAAsF;oBACtF,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC;iBAC5D,CAAC,CAAC;gBACH,OAAO,GAAG,CAAC;YACZ,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,MAAwB,EAAE,EAAE;gBAC3C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC;oBACpD,UAAU,EAAE,cAAc;oBAC1B,KAAK,EAAE,MAAM;iBACb,CAAC,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAChB,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE;oBACrC,sFAAsF;oBACtF,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC;iBAC5D,CAAC,CACF,CACD,CAAC;gBACF,OAAO,IAAI,CAAC;YACb,CAAC;SACD,CAAC;QACF,OAAO,GAAG,CAAC;IACZ,CAAC,EAAE,EAAS,CAAC,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EACxC,SAAS,EACT,OAAO,EACP,EAAE,GAMF;IACA,MAAM,gBAAgB,mCAClB,OAAO,KACV,MAAM,EAAE,SAAS,CAAC,SAAS,GAC3B,CAAC;IACF,IAAI,SAAS,CAAC,SAAS,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACvC,OAAO,yBAAyB,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,KAAK,EAAoB,CAAC;IAE9C,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,mBAAmB,CAAC;QACnC,SAAS;QACT,OAAO,EAAE,gBAAgB;QACzB,SAAS;KACT,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,qBAAqB,CAAC;QACvC,SAAS;QACT,OAAO;QACP,GAAG,EAAE,gBAAgB;KACrB,CAAC,CAAC;IACH,MAAM,gBAAgB,GAAG,KAAK,EAAE,UAAkB,EAAE,EAAE;QACrD,MAAM,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC,CAAC;IACF,MAAM,UAAU,GAAG,IAAI,KAAK,EAAgB,CAAC;IAC7C,MAAM,MAAM,GAAoB;QAC/B,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,OAAO;QACP,gBAAgB;QAChB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE;YACvC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;YACjD,OAAO,CAAC,GAAG,CACV,OAAO,EACP,aAAa,IAAI,CAAC,MAAM,iBAAiB,UAAU,EAAE,CACrD,CAAC;YAEF,MAAM,OAAO,CAAC,GAAG,CAChB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,GAAQ,EAAE,EAAE;gBAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC5B,MAAM,CACL,CAAC,CAAC,OAAO,EACT,+BAA+B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CACpD,CAAC;gBACF,6DAA6D;gBAC7D,mEAAmE;gBACnE,4DAA4D;gBAC5D,mDAAmD;gBACnD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;gBAC3D,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;gBAChC,yCAAyC;gBACzC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;gBACrC,IAAI,QAAQ,EAAE,CAAC;oBACd,+DAA+D;oBAC/D,gEAAgE;oBAChE,oBAAoB;oBACpB,oCAAoC,CAAC,QAAQ,CAAC,CAAC;oBAC/C,oCAAoC,CAAC,QAAQ,CAAC,CAAC;oBAC/C,yBAAyB,CAAC,QAAQ,CAAC,CAAC;oBACpC,MAAM,OAAO,GAAG,aAAa,CAC5B,QAAQ,EACR,QAAQ,EACR,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,OAAO,CAAC,EACrD,SAAS,EACT,EAAE,EACF;wBACC,mBAAmB,EAAE,IAAI;wBACzB,KAAK;qBACL,CACD,CAAC;oBACF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACxB,MAAM,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;4BAC7B,UAAU,EAAE,OAAO;4BACnB,OAAO,EAAE,IAAI;yBACb,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC,CAAC,CACF,CAAC;QACH,CAAC;QACD,OAAO;QACP,SAAS;QACT,UAAU;QACV,KAAK,EAAE,KAAK,IAAI,EAAE;YACjB,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;KACD,CAAC;IACF,OAAO,MAAM,CAAC;AACf,CAAC;AAED,SAAS,yBAAyB,CAAC,EAClC,SAAS,EACT,OAAO,GAIP;IACA,MAAM,OAAO,GAAG,IAAI,KAAK,EAAoB,CAAC;IAE9C,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,EAAS,EAAE;QACpC,GAAG;YACF,MAAM,IAAI,KAAK,CACd,4EAA4E,CAC5E,CAAC;QACH,CAAC;KACD,CAAQ,CAAC;IAEV,MAAM,SAAS,GAAG,qBAAqB,CAAC;QACvC,SAAS;QACT,OAAO;QACP,GAAG,EAAE,OAAO;KACZ,CAAC,CAAC;IACH,MAAM,MAAM,GAAoB;QAC/B,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,OAAO;QACP,gBAAgB,EAAE,GAAG,EAAE;YACtB,MAAM,IAAI,KAAK,CACd,iIAAiI,CACjI,CAAC;QACH,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACb,MAAM,IAAI,KAAK,CACd,wHAAwH,CACxH,CAAC;QACH,CAAC;QACD,OAAO;QACP,SAAS;QACT,UAAU,EAAE,EAAE;QACd,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;KAC9B,CAAC;IACF,OAAO,MAAM,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../../src/persistence/migration/engine.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verdant-web/store",
3
- "version": "4.1.6",
3
+ "version": "4.3.0",
4
4
  "access": "public",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",
@@ -34,7 +34,7 @@
34
34
  "jszip": "^3.10.1",
35
35
  "jwt-decode": "^3.1.2",
36
36
  "weak-event": "^2.0.5",
37
- "@verdant-web/common": "2.7.3"
37
+ "@verdant-web/common": "2.9.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "20.10.5",
@@ -13,6 +13,13 @@ export class UndoHistory extends EventSubscriber<{ change: () => void }> {
13
13
  return this._undone.length > 0;
14
14
  }
15
15
 
16
+ get undoLength() {
17
+ return this._undoable.length;
18
+ }
19
+ get redoLength() {
20
+ return this._undone.length;
21
+ }
22
+
16
23
  undo = async () => {
17
24
  const next = this._undoable.pop();
18
25
  if (next) {
@@ -1,7 +1,17 @@
1
- import { assert } from '@verdant-web/common';
2
- import { describe, it, expect, vi, MockedFunction, vitest } from 'vitest';
1
+ import {
2
+ assert,
3
+ createRef,
4
+ EventSubscriber,
5
+ NaiveTimestampProvider,
6
+ PatchCreator,
7
+ schema,
8
+ } from '@verdant-web/common';
9
+ import { describe, expect, it, MockedFunction, vi, vitest } from 'vitest';
10
+ import { WeakEvent } from 'weak-event';
11
+ import { Time } from '../context/Time.js';
12
+ import { EntityFamilyMetadata } from '../entities/EntityMetadata.js';
13
+ import { Entity, EntityFile } from '../index.js';
3
14
  import { createTestStorage } from './fixtures/testStorage.js';
4
- import { EntityFile } from '../index.js';
5
15
 
6
16
  async function waitForStoragePropagation(mock: MockedFunction<any>) {
7
17
  await new Promise<void>((resolve, reject) => {
@@ -639,7 +649,7 @@ describe('entities', () => {
639
649
  content: { invalid: 'value' },
640
650
  });
641
651
  }).rejects.toThrowErrorMatchingInlineSnapshot(
642
- `[Error: Validation error: Expected string for field content, got [object Object]]`,
652
+ `[Error: Validation error: Expected string for field content, got {"invalid":"value"}]`,
643
653
  );
644
654
  });
645
655
 
@@ -650,7 +660,7 @@ describe('entities', () => {
650
660
  expect(() => {
651
661
  weird.set('file', { invalid: 'value' });
652
662
  }).toThrowErrorMatchingInlineSnapshot(
653
- `[Error: Validation error: Expected file or null for field file, got [object Object]]`,
663
+ `[Error: Validation error: Expected file or null for field file, got {"invalid":"value"}]`,
654
664
  );
655
665
 
656
666
  // valid options
@@ -705,4 +715,149 @@ describe('entities', () => {
705
715
 
706
716
  expect(item.deleted).toBe(true);
707
717
  });
718
+
719
+ it('should apply contextual changes to a pruned entity in a way consistent with the pruned view of the data', async () => {
720
+ // manually constructing an entity for this test is easiest,
721
+ // kind of hard to force invalid data otherwise
722
+ const onPendingOperations = vi.fn();
723
+ const time = new Time(new NaiveTimestampProvider(), 1);
724
+ // too much junk in here, have to manually pick and choose
725
+ let subId = 0;
726
+ const testCtx = {
727
+ globalEvents: new EventSubscriber(),
728
+ time,
729
+ log: vi.fn(),
730
+ patchCreator: new PatchCreator(
731
+ () => time.now,
732
+ () => `${subId++}`,
733
+ ),
734
+ files: {
735
+ add: vi.fn(),
736
+ },
737
+ } as any;
738
+ const metadataFamily = new EntityFamilyMetadata({
739
+ ctx: testCtx,
740
+ onPendingOperations,
741
+ rootOid: 'foos/a',
742
+ });
743
+ const entity = new Entity({
744
+ oid: 'foos/a',
745
+ schema: {
746
+ type: 'object',
747
+ properties: {
748
+ id: schema.fields.id(),
749
+ content: schema.fields.string(),
750
+ items: schema.fields.array({
751
+ items: schema.fields.object({
752
+ properties: {
753
+ content: schema.fields.string(),
754
+ },
755
+ }),
756
+ }),
757
+ },
758
+ },
759
+ ctx: testCtx,
760
+ deleteSelf: vi.fn(),
761
+ files: {
762
+ add: vi.fn(),
763
+ } as any,
764
+ metadataFamily,
765
+ storeEvents: {
766
+ add: new WeakEvent(),
767
+ replace: new WeakEvent(),
768
+ resetAll: new WeakEvent(),
769
+ },
770
+ readonlyKeys: ['id'],
771
+ });
772
+ metadataFamily.addConfirmedData({
773
+ baselines: [
774
+ {
775
+ oid: 'foos/a:1',
776
+ snapshot: { content: 'item 1' },
777
+ timestamp: time.now,
778
+ },
779
+ {
780
+ oid: 'foos/a:2',
781
+ snapshot: { content: 'item 2' },
782
+ timestamp: time.now,
783
+ },
784
+ {
785
+ oid: 'foos/a:3',
786
+ snapshot: {}, // INVALID!
787
+ timestamp: time.now,
788
+ },
789
+ {
790
+ oid: 'foos/a:4',
791
+ snapshot: { content: 'item 4' },
792
+ timestamp: time.now,
793
+ },
794
+ {
795
+ oid: 'foos/a:5',
796
+ snapshot: [
797
+ createRef('foos/a:1'),
798
+ createRef('foos/a:2'),
799
+ createRef('foos/a:3'),
800
+ createRef('foos/a:4'),
801
+ ],
802
+ timestamp: time.now,
803
+ },
804
+ {
805
+ oid: 'foos/a',
806
+ snapshot: {
807
+ id: 'a',
808
+ content: 'the main foo',
809
+ items: createRef('foos/a:5'),
810
+ },
811
+ timestamp: time.now,
812
+ },
813
+ ],
814
+ });
815
+
816
+ expect(entity.deepInvalid).toBe(true);
817
+
818
+ // check all that worked, lol. and that it's
819
+ // pruned item 3
820
+ expect(entity.getSnapshot()).toEqual({
821
+ id: 'a',
822
+ content: 'the main foo',
823
+ items: [
824
+ { content: 'item 1' },
825
+ { content: 'item 2' },
826
+ { content: 'item 4' },
827
+ ],
828
+ });
829
+
830
+ // also check that unpruned snapshot is correct
831
+ expect(entity.getUnprunedSnapshot()).toEqual({
832
+ id: 'a',
833
+ content: 'the main foo',
834
+ items: [
835
+ { content: 'item 1' },
836
+ { content: 'item 2' },
837
+ {},
838
+ { content: 'item 4' },
839
+ ],
840
+ });
841
+
842
+ // now, let's set the content of index 2.
843
+ // if this works as expected, we should replace
844
+ // item 4 (even though technically it's at index 3)
845
+ // because we are respecting the user's intention.
846
+ const items = entity.get('items');
847
+ items.set(2, { content: 'new item' });
848
+
849
+ expect(entity.getSnapshot()).toEqual({
850
+ id: 'a',
851
+ content: 'the main foo',
852
+ items: [
853
+ { content: 'item 1' },
854
+ { content: 'item 2' },
855
+ { content: 'new item' },
856
+ ],
857
+ });
858
+
859
+ // now, the entity has been 'fixed' and is valid again
860
+ expect(entity.getSnapshot()).toEqual(entity.getUnprunedSnapshot());
861
+ expect(entity.deepInvalid).toBe(false);
862
+ });
708
863
  });
@@ -92,7 +92,7 @@ export const weirdCollection = schema.collection({
92
92
  },
93
93
  });
94
94
 
95
- const testSchema = schema({
95
+ export const testSchema = schema({
96
96
  version: 1,
97
97
  collections: {
98
98
  todos: todoCollection,
@@ -85,6 +85,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
85
85
 
86
86
  constructor(private context: Context) {
87
87
  super();
88
+ context.getClient = () => this;
88
89
  this.collectionNames = Object.keys(context.schema.collections);
89
90
  this._sync =
90
91
  this.context.config.sync && !context.schema.wip
@@ -4,6 +4,7 @@ import {
4
4
  Migration,
5
5
  PatchCreator,
6
6
  StorageSchema,
7
+ VerdantError,
7
8
  noop,
8
9
  } from '@verdant-web/common';
9
10
  import { FileConfig, InitialContext, QueryConfig } from '../context/context.js';
@@ -167,6 +168,13 @@ export class ClientDescriptor<
167
168
  environment,
168
169
  persistenceShutdownHandler: new ShutdownHandler(init.log),
169
170
  pauseRebasing: false,
171
+ getClient() {
172
+ throw new VerdantError(
173
+ VerdantError.Code.Unexpected,
174
+ undefined,
175
+ 'Client not yet initialized. This is a Verdant bug, please report it.',
176
+ );
177
+ },
170
178
  };
171
179
  ctx.log('info', 'Initializing client', {
172
180
  namespace: ctx.namespace,
@@ -10,6 +10,7 @@ import {
10
10
  StorageSchema,
11
11
  } from '@verdant-web/common';
12
12
  import { UndoHistory } from '../UndoHistory.js';
13
+ import type { Client } from '../client/Client.js';
13
14
  import { PersistenceFiles } from '../persistence/PersistenceFiles.js';
14
15
  import type { PersistenceMetadata } from '../persistence/PersistenceMetadata.js';
15
16
  import type { PersistenceDocuments } from '../persistence/PersistenceQueries.js';
@@ -110,6 +111,12 @@ export interface Context {
110
111
  };
111
112
 
112
113
  persistence: PersistenceImplementation;
114
+
115
+ /**
116
+ * Must be defined by the Client once it exists. Attempts to use this before
117
+ * it's ready will rightfully throw an error.
118
+ */
119
+ getClient: () => Client;
113
120
  }
114
121
 
115
122
  export interface FileConfig {
@@ -17,6 +17,7 @@ import {
17
17
  isFileRef,
18
18
  isNullable,
19
19
  isObject,
20
+ isObjectRef,
20
21
  isRef,
21
22
  maybeGetOid,
22
23
  memoByKeys,
@@ -26,12 +27,13 @@ import {
26
27
  import { Context } from '../context/context.js';
27
28
  import { FileManager } from '../files/FileManager.js';
28
29
  import { processValueFiles } from '../files/utils.js';
29
- import { EntityFile } from '../index.js';
30
+ import { ClientWithCollections, EntityFile } from '../index.js';
30
31
  import { EntityCache } from './EntityCache.js';
31
32
  import { EntityFamilyMetadata, EntityMetadataView } from './EntityMetadata.js';
32
33
  import { EntityStoreEventData, EntityStoreEvents } from './EntityStore.js';
33
34
  import { entityFieldSubscriber } from './entityFieldSubscriber.js';
34
35
  import {
36
+ AnyEntity,
35
37
  BaseEntityValue,
36
38
  DataFromInit,
37
39
  EntityChange,
@@ -56,6 +58,8 @@ export interface EntityInit {
56
58
  deleteSelf: () => void;
57
59
  }
58
60
 
61
+ const PRIVATE_ENTITY_CONTEXT_KEY = Symbol('private entity context key');
62
+
59
63
  export class Entity<
60
64
  Init = any,
61
65
  KeyValue extends BaseEntityValue = any,
@@ -79,6 +83,10 @@ export class Entity<
79
83
  private files;
80
84
  private storeEvents;
81
85
 
86
+ get [PRIVATE_ENTITY_CONTEXT_KEY]() {
87
+ return this.ctx;
88
+ }
89
+
82
90
  // an internal representation of this Entity.
83
91
  // if present, this is the cached, known value. If null,
84
92
  // the entity is deleted. If undefined, we need to recompute
@@ -290,7 +298,12 @@ export class Entity<
290
298
  break;
291
299
  }
292
300
  } else {
293
- this.cachedView[key] = child;
301
+ // special case - rewrite undefined fields to null
302
+ if (isNullable(schema) && child === undefined) {
303
+ this.cachedView[key] = null;
304
+ } else {
305
+ this.cachedView[key] = child;
306
+ }
294
307
  }
295
308
  }
296
309
  }
@@ -318,6 +331,33 @@ export class Entity<
318
331
  return !!this.validate();
319
332
  }
320
333
 
334
+ /**
335
+ * Returns true if this or any child is invalid (pruned)
336
+ */
337
+ get deepInvalid(): boolean {
338
+ if (this.invalid) return true;
339
+ if (Array.isArray(this.rawView)) {
340
+ for (let i = 0; i < this.rawView.length; i++) {
341
+ if (isObjectRef(this.rawView[i])) {
342
+ const child = this.getChild(i, this.rawView[i].id);
343
+ if (child.deepInvalid) {
344
+ return true;
345
+ }
346
+ }
347
+ }
348
+ } else if (isObject(this.rawView)) {
349
+ for (const key in this.rawView) {
350
+ if (isObjectRef(this.rawView[key])) {
351
+ const child = this.getChild(key, this.rawView[key].id);
352
+ if (child.deepInvalid) {
353
+ return true;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ return false;
359
+ }
360
+
321
361
  get isList() {
322
362
  // have to turn TS off here as our two interfaces both implement
323
363
  // const values for this boolean.
@@ -411,7 +451,7 @@ export class Entity<
411
451
  field: this.schema,
412
452
  value: this.rawView,
413
453
  fieldPath: this.fieldPath,
414
- depth: 1,
454
+ expectRefs: true,
415
455
  }) ?? undefined;
416
456
  return this.validationError;
417
457
  },
@@ -449,6 +489,37 @@ export class Entity<
449
489
  }
450
490
  };
451
491
 
492
+ private rawViewWithMappedChildren = (
493
+ mapper: (child: Entity | EntityFile) => any,
494
+ ) => {
495
+ const view = this.rawView;
496
+ if (!view) {
497
+ return null;
498
+ }
499
+ if (Array.isArray(view)) {
500
+ const mapped = view.map((value, i) => {
501
+ if (isRef(value)) {
502
+ return mapper(this.getChild(i, value.id));
503
+ } else {
504
+ return value;
505
+ }
506
+ });
507
+ assignOid(mapped, this.oid);
508
+ return mapped;
509
+ } else {
510
+ const mapped = Object.entries(view).reduce((acc, [key, value]) => {
511
+ if (isRef(value)) {
512
+ acc[key as any] = mapper(this.getChild(key, value.id));
513
+ } else {
514
+ acc[key as any] = value;
515
+ }
516
+ return acc;
517
+ }, {} as any);
518
+ assignOid(mapped, this.oid);
519
+ return mapped;
520
+ }
521
+ };
522
+
452
523
  /**
453
524
  * A current snapshot of this Entity's data, including nested
454
525
  * Entities.
@@ -457,10 +528,62 @@ export class Entity<
457
528
  return this.viewWithMappedChildren((child) => child.getSnapshot());
458
529
  };
459
530
 
531
+ /**
532
+ * A snapshot of this Entity with unpruned (invalid) data. This will
533
+ * not conform to the entity schema and should be used carefully.
534
+ *
535
+ * Can be used to inspect or recover invalid, pruned data not
536
+ * otherwise accessible.
537
+ */
538
+ getUnprunedSnapshot = (): any => {
539
+ return this.rawViewWithMappedChildren((child) => {
540
+ if (child instanceof EntityFile) return child.getSnapshot();
541
+ return child.getUnprunedSnapshot();
542
+ });
543
+ };
544
+
460
545
  // change management methods (internal use only)
461
546
  private addPendingOperations = (operations: Operation[]) => {
462
- this.ctx.log('debug', 'Entity: adding pending operations', this.oid);
547
+ this.ctx.log(
548
+ 'debug',
549
+ 'Entity: adding pending operations',
550
+ this.oid,
551
+ operations,
552
+ );
553
+
554
+ // special case -- if this entity is pruned, any changes we apply to it
555
+ // will be in relation to 'exposed' pruned data, not the 'real world'
556
+ // data that's backing it. That means those changes will produce unexpected
557
+ // or further invalid results. To avoid this, we basically stamp in the
558
+ // pruned version of this entity before proceeding.
559
+ //
560
+ // as an example of a failure mode without this check, consider a list:
561
+ // [1, 2, <pruned>, 4, 5]
562
+ // the user sees: [1, 2, 4, 5]
563
+ // when they try to replace the item at index 2 with "0" (they see "4"), they
564
+ // actually replace the invisible pruned item, resulting in [1, 2, 0, 4, 5]
565
+ // being the result when they expected [1, 2, 0, 5].
566
+ //
567
+ // To "stamp" the data before applying user changes, we diff the snapshot
568
+ // (which is the pruned version) with the current state of the entity.
569
+ if (this.deepInvalid) {
570
+ this.ctx.log(
571
+ 'warn',
572
+ 'Changes are being applied to a pruned entity. This means that the pruned version is being treated as the new baseline and any pruned invalid data is lost.',
573
+ this.oid,
574
+ );
575
+ this.canonizePrunedVersion();
576
+ }
577
+
578
+ this.applyPendingOperations(operations);
579
+ };
463
580
 
581
+ // naming is fuzzy here, but this method was split out from
582
+ // addPendingOperations since that method also conditionally canonizes
583
+ // the pruned snapshot, and I wanted to keep the actual insertion of
584
+ // the ops in one place, so leaving it as part of addPendingOperations
585
+ // would introduce infinite recursion when canonizing.
586
+ private applyPendingOperations = (operations: Operation[]) => {
464
587
  // apply authz to all operations
465
588
  if (this.access) {
466
589
  for (const op of operations) {
@@ -474,6 +597,21 @@ export class Entity<
474
597
  }
475
598
  };
476
599
 
600
+ private canonizePrunedVersion = () => {
601
+ const snapshot = this.getSnapshot();
602
+ const unprunedSnapshot = this.getUnprunedSnapshot();
603
+ const operations = this.patchCreator.createDiff(
604
+ unprunedSnapshot,
605
+ snapshot,
606
+ {
607
+ authz: this.access,
608
+ merge: false,
609
+ },
610
+ );
611
+
612
+ this.applyPendingOperations(operations);
613
+ };
614
+
477
615
  private addConfirmedData = (data: EntityStoreEventData) => {
478
616
  this.ctx.log('debug', 'Entity: adding confirmed data', this.oid);
479
617
  const changes = this.metadataFamily.addConfirmedData(data);
@@ -920,7 +1058,7 @@ export class Entity<
920
1058
  this.addPendingOperations(
921
1059
  this.patchCreator.createDiff(this.getSnapshot(), changes, {
922
1060
  mergeUnknownObjects: !replaceSubObjects,
923
- defaultUndefined: merge,
1061
+ merge,
924
1062
  }),
925
1063
  );
926
1064
  };
@@ -1126,3 +1264,11 @@ function assertNumber(key: unknown): asserts key is number {
1126
1264
  if (typeof key !== 'number')
1127
1265
  throw new Error('Only number keys are supported in list entities');
1128
1266
  }
1267
+
1268
+ export function getEntityClient(
1269
+ entity: AnyEntity<any, any, any>,
1270
+ ): ClientWithCollections {
1271
+ return (entity as Entity)[
1272
+ PRIVATE_ENTITY_CONTEXT_KEY
1273
+ ].getClient() as ClientWithCollections;
1274
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AuthorizationKey,
2
3
  DocumentBaseline,
3
4
  ObjectIdentifier,
4
5
  Operation,
@@ -20,7 +21,7 @@ export type EntityMetadataView = {
20
21
  empty: boolean;
21
22
  updatedAt: number;
22
23
  latestTimestamp: string | null;
23
- authz?: string;
24
+ authz?: AuthorizationKey;
24
25
  };
25
26
 
26
27
  export class EntityMetadata {
@@ -101,7 +102,7 @@ export class EntityMetadata {
101
102
  // we don't use after for pending ops, they're all
102
103
  // logically in the future
103
104
  null,
104
- );
105
+ );
105
106
  if (pendingResult.authz) {
106
107
  authz = pendingResult.authz;
107
108
  }
@@ -221,10 +222,10 @@ export class EntityMetadata {
221
222
  latestTimestamp: string | null;
222
223
  deleted: boolean;
223
224
  futureSeen: string | undefined;
224
- authz?: string;
225
+ authz?: AuthorizationKey;
225
226
  } => {
226
227
  let futureSeen: string | undefined = undefined;
227
- let authz: string | undefined = undefined;
228
+ let authz: AuthorizationKey | undefined = undefined;
228
229
 
229
230
  const now = this.ctx.time.now;
230
231
  for (const op of operations) {