@verdant-web/store 2.8.5 → 3.0.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 (278) hide show
  1. package/dist/bundle/index.js +9 -10
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/cjs/DocumentManager.d.ts +1 -1
  4. package/dist/cjs/DocumentManager.js +1 -1
  5. package/dist/cjs/DocumentManager.js.map +1 -1
  6. package/dist/cjs/IDBService.d.ts +28 -7
  7. package/dist/cjs/IDBService.js +50 -13
  8. package/dist/cjs/IDBService.js.map +1 -1
  9. package/dist/cjs/UndoHistory.d.ts +1 -1
  10. package/dist/cjs/UndoHistory.js +6 -2
  11. package/dist/cjs/UndoHistory.js.map +1 -1
  12. package/dist/cjs/__tests__/batching.test.js +3 -1
  13. package/dist/cjs/__tests__/batching.test.js.map +1 -1
  14. package/dist/cjs/__tests__/documents.test.js +37 -6
  15. package/dist/cjs/__tests__/documents.test.js.map +1 -1
  16. package/dist/cjs/__tests__/fixtures/testStorage.d.ts +2 -2
  17. package/dist/cjs/__tests__/fixtures/testStorage.js +2 -1
  18. package/dist/cjs/__tests__/fixtures/testStorage.js.map +1 -1
  19. package/dist/cjs/__tests__/legacyOids.test.js +50 -17
  20. package/dist/cjs/__tests__/legacyOids.test.js.map +1 -1
  21. package/dist/cjs/__tests__/mutations.test.js +9 -3
  22. package/dist/cjs/__tests__/mutations.test.js.map +1 -1
  23. package/dist/cjs/__tests__/queries.test.js +6 -2
  24. package/dist/cjs/__tests__/queries.test.js.map +1 -1
  25. package/dist/cjs/__tests__/setup/indexedDB.d.ts +1 -1
  26. package/dist/cjs/__tests__/setup/indexedDB.js +8 -1
  27. package/dist/cjs/__tests__/setup/indexedDB.js.map +1 -1
  28. package/dist/cjs/__tests__/undo.test.js +16 -9
  29. package/dist/cjs/__tests__/undo.test.js.map +1 -1
  30. package/dist/cjs/client/Client.d.ts +1 -1
  31. package/dist/cjs/client/Client.js +7 -3
  32. package/dist/cjs/client/Client.js.map +1 -1
  33. package/dist/cjs/client/ClientDescriptor.js +21 -6
  34. package/dist/cjs/client/ClientDescriptor.js.map +1 -1
  35. package/dist/cjs/context.d.ts +10 -1
  36. package/dist/cjs/entities/Entity.d.ts +106 -178
  37. package/dist/cjs/entities/Entity.js +558 -376
  38. package/dist/cjs/entities/Entity.js.map +1 -1
  39. package/dist/cjs/entities/Entity.test.d.ts +1 -0
  40. package/dist/cjs/entities/Entity.test.js +194 -0
  41. package/dist/cjs/entities/Entity.test.js.map +1 -0
  42. package/dist/cjs/entities/EntityCache.d.ts +15 -0
  43. package/dist/cjs/entities/EntityCache.js +39 -0
  44. package/dist/cjs/entities/EntityCache.js.map +1 -0
  45. package/dist/cjs/entities/EntityMetadata.d.ts +68 -0
  46. package/dist/cjs/entities/EntityMetadata.js +261 -0
  47. package/dist/cjs/entities/EntityMetadata.js.map +1 -0
  48. package/dist/cjs/entities/EntityStore.d.ts +63 -68
  49. package/dist/cjs/entities/EntityStore.js +294 -438
  50. package/dist/cjs/entities/EntityStore.js.map +1 -1
  51. package/dist/cjs/entities/OperationBatcher.d.ts +52 -0
  52. package/dist/cjs/entities/OperationBatcher.js +165 -0
  53. package/dist/cjs/entities/OperationBatcher.js.map +1 -0
  54. package/dist/cjs/entities/types.d.ts +84 -0
  55. package/dist/cjs/entities/types.js +3 -0
  56. package/dist/cjs/entities/types.js.map +1 -0
  57. package/dist/cjs/files/EntityFile.d.ts +5 -2
  58. package/dist/cjs/files/EntityFile.js +8 -4
  59. package/dist/cjs/files/EntityFile.js.map +1 -1
  60. package/dist/cjs/files/FileManager.d.ts +3 -1
  61. package/dist/cjs/files/FileManager.js +5 -3
  62. package/dist/cjs/files/FileManager.js.map +1 -1
  63. package/dist/cjs/files/FileStorage.js +7 -7
  64. package/dist/cjs/files/FileStorage.js.map +1 -1
  65. package/dist/cjs/files/utils.d.ts +2 -0
  66. package/dist/cjs/files/utils.js +5 -2
  67. package/dist/cjs/files/utils.js.map +1 -1
  68. package/dist/cjs/idb.d.ts +2 -0
  69. package/dist/cjs/idb.js +50 -4
  70. package/dist/cjs/idb.js.map +1 -1
  71. package/dist/cjs/index.d.ts +1 -1
  72. package/dist/cjs/metadata/AckInfoStore.js +1 -1
  73. package/dist/cjs/metadata/AckInfoStore.js.map +1 -1
  74. package/dist/cjs/metadata/BaselinesStore.d.ts +4 -1
  75. package/dist/cjs/metadata/BaselinesStore.js +19 -10
  76. package/dist/cjs/metadata/BaselinesStore.js.map +1 -1
  77. package/dist/cjs/metadata/LocalReplicaStore.d.ts +1 -1
  78. package/dist/cjs/metadata/LocalReplicaStore.js +11 -5
  79. package/dist/cjs/metadata/LocalReplicaStore.js.map +1 -1
  80. package/dist/cjs/metadata/Metadata.d.ts +26 -5
  81. package/dist/cjs/metadata/Metadata.js +55 -18
  82. package/dist/cjs/metadata/Metadata.js.map +1 -1
  83. package/dist/cjs/metadata/OperationsStore.d.ts +3 -0
  84. package/dist/cjs/metadata/OperationsStore.js +35 -15
  85. package/dist/cjs/metadata/OperationsStore.js.map +1 -1
  86. package/dist/cjs/migration/openDatabase.js +31 -10
  87. package/dist/cjs/migration/openDatabase.js.map +1 -1
  88. package/dist/cjs/queries/BaseQuery.js +13 -1
  89. package/dist/cjs/queries/BaseQuery.js.map +1 -1
  90. package/dist/cjs/queries/CollectionQueries.js +1 -1
  91. package/dist/cjs/queries/CollectionQueries.js.map +1 -1
  92. package/dist/cjs/queries/FindAllQuery.js +1 -0
  93. package/dist/cjs/queries/FindAllQuery.js.map +1 -1
  94. package/dist/cjs/queries/QueryCache.d.ts +1 -0
  95. package/dist/cjs/queries/QueryCache.js +4 -0
  96. package/dist/cjs/queries/QueryCache.js.map +1 -1
  97. package/dist/cjs/queries/QueryableStorage.d.ts +20 -0
  98. package/dist/cjs/queries/QueryableStorage.js +84 -0
  99. package/dist/cjs/queries/QueryableStorage.js.map +1 -0
  100. package/dist/cjs/queries/dbQueries.js +13 -3
  101. package/dist/cjs/queries/dbQueries.js.map +1 -1
  102. package/dist/cjs/sync/FileSync.d.ts +1 -0
  103. package/dist/cjs/sync/FileSync.js +1 -0
  104. package/dist/cjs/sync/FileSync.js.map +1 -1
  105. package/dist/cjs/sync/PushPullSync.d.ts +2 -1
  106. package/dist/cjs/sync/PushPullSync.js +7 -1
  107. package/dist/cjs/sync/PushPullSync.js.map +1 -1
  108. package/dist/cjs/sync/Sync.d.ts +6 -3
  109. package/dist/cjs/sync/Sync.js +9 -4
  110. package/dist/cjs/sync/Sync.js.map +1 -1
  111. package/dist/cjs/sync/WebSocketSync.d.ts +4 -1
  112. package/dist/cjs/sync/WebSocketSync.js +41 -11
  113. package/dist/cjs/sync/WebSocketSync.js.map +1 -1
  114. package/dist/esm/DocumentManager.d.ts +1 -1
  115. package/dist/esm/DocumentManager.js +1 -1
  116. package/dist/esm/DocumentManager.js.map +1 -1
  117. package/dist/esm/IDBService.d.ts +28 -7
  118. package/dist/esm/IDBService.js +51 -14
  119. package/dist/esm/IDBService.js.map +1 -1
  120. package/dist/esm/UndoHistory.d.ts +1 -1
  121. package/dist/esm/UndoHistory.js +6 -2
  122. package/dist/esm/UndoHistory.js.map +1 -1
  123. package/dist/esm/__tests__/batching.test.js +3 -1
  124. package/dist/esm/__tests__/batching.test.js.map +1 -1
  125. package/dist/esm/__tests__/documents.test.js +37 -6
  126. package/dist/esm/__tests__/documents.test.js.map +1 -1
  127. package/dist/esm/__tests__/fixtures/testStorage.d.ts +2 -2
  128. package/dist/esm/__tests__/fixtures/testStorage.js +2 -1
  129. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  130. package/dist/esm/__tests__/legacyOids.test.js +50 -17
  131. package/dist/esm/__tests__/legacyOids.test.js.map +1 -1
  132. package/dist/esm/__tests__/mutations.test.js +9 -3
  133. package/dist/esm/__tests__/mutations.test.js.map +1 -1
  134. package/dist/esm/__tests__/queries.test.js +6 -2
  135. package/dist/esm/__tests__/queries.test.js.map +1 -1
  136. package/dist/esm/__tests__/setup/indexedDB.d.ts +1 -1
  137. package/dist/esm/__tests__/setup/indexedDB.js +8 -1
  138. package/dist/esm/__tests__/setup/indexedDB.js.map +1 -1
  139. package/dist/esm/__tests__/undo.test.js +16 -9
  140. package/dist/esm/__tests__/undo.test.js.map +1 -1
  141. package/dist/esm/client/Client.d.ts +1 -1
  142. package/dist/esm/client/Client.js +7 -3
  143. package/dist/esm/client/Client.js.map +1 -1
  144. package/dist/esm/client/ClientDescriptor.js +21 -6
  145. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  146. package/dist/esm/context.d.ts +10 -1
  147. package/dist/esm/entities/Entity.d.ts +106 -178
  148. package/dist/esm/entities/Entity.js +559 -376
  149. package/dist/esm/entities/Entity.js.map +1 -1
  150. package/dist/esm/entities/Entity.test.d.ts +1 -0
  151. package/dist/esm/entities/Entity.test.js +192 -0
  152. package/dist/esm/entities/Entity.test.js.map +1 -0
  153. package/dist/esm/entities/EntityCache.d.ts +15 -0
  154. package/dist/esm/entities/EntityCache.js +35 -0
  155. package/dist/esm/entities/EntityCache.js.map +1 -0
  156. package/dist/esm/entities/EntityMetadata.d.ts +68 -0
  157. package/dist/esm/entities/EntityMetadata.js +256 -0
  158. package/dist/esm/entities/EntityMetadata.js.map +1 -0
  159. package/dist/esm/entities/EntityStore.d.ts +63 -68
  160. package/dist/esm/entities/EntityStore.js +295 -439
  161. package/dist/esm/entities/EntityStore.js.map +1 -1
  162. package/dist/esm/entities/OperationBatcher.d.ts +52 -0
  163. package/dist/esm/entities/OperationBatcher.js +161 -0
  164. package/dist/esm/entities/OperationBatcher.js.map +1 -0
  165. package/dist/esm/entities/types.d.ts +84 -0
  166. package/dist/esm/entities/types.js +2 -0
  167. package/dist/esm/entities/types.js.map +1 -0
  168. package/dist/esm/files/EntityFile.d.ts +5 -2
  169. package/dist/esm/files/EntityFile.js +8 -4
  170. package/dist/esm/files/EntityFile.js.map +1 -1
  171. package/dist/esm/files/FileManager.d.ts +3 -1
  172. package/dist/esm/files/FileManager.js +5 -3
  173. package/dist/esm/files/FileManager.js.map +1 -1
  174. package/dist/esm/files/FileStorage.js +7 -7
  175. package/dist/esm/files/FileStorage.js.map +1 -1
  176. package/dist/esm/files/utils.d.ts +2 -0
  177. package/dist/esm/files/utils.js +4 -2
  178. package/dist/esm/files/utils.js.map +1 -1
  179. package/dist/esm/idb.d.ts +2 -0
  180. package/dist/esm/idb.js +47 -3
  181. package/dist/esm/idb.js.map +1 -1
  182. package/dist/esm/index.d.ts +1 -1
  183. package/dist/esm/metadata/AckInfoStore.js +1 -1
  184. package/dist/esm/metadata/AckInfoStore.js.map +1 -1
  185. package/dist/esm/metadata/BaselinesStore.d.ts +4 -1
  186. package/dist/esm/metadata/BaselinesStore.js +19 -10
  187. package/dist/esm/metadata/BaselinesStore.js.map +1 -1
  188. package/dist/esm/metadata/LocalReplicaStore.d.ts +1 -1
  189. package/dist/esm/metadata/LocalReplicaStore.js +11 -5
  190. package/dist/esm/metadata/LocalReplicaStore.js.map +1 -1
  191. package/dist/esm/metadata/Metadata.d.ts +26 -5
  192. package/dist/esm/metadata/Metadata.js +56 -19
  193. package/dist/esm/metadata/Metadata.js.map +1 -1
  194. package/dist/esm/metadata/OperationsStore.d.ts +3 -0
  195. package/dist/esm/metadata/OperationsStore.js +35 -15
  196. package/dist/esm/metadata/OperationsStore.js.map +1 -1
  197. package/dist/esm/migration/openDatabase.js +32 -11
  198. package/dist/esm/migration/openDatabase.js.map +1 -1
  199. package/dist/esm/queries/BaseQuery.js +13 -1
  200. package/dist/esm/queries/BaseQuery.js.map +1 -1
  201. package/dist/esm/queries/CollectionQueries.js +1 -1
  202. package/dist/esm/queries/CollectionQueries.js.map +1 -1
  203. package/dist/esm/queries/FindAllQuery.js +1 -0
  204. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  205. package/dist/esm/queries/QueryCache.d.ts +1 -0
  206. package/dist/esm/queries/QueryCache.js +4 -0
  207. package/dist/esm/queries/QueryCache.js.map +1 -1
  208. package/dist/esm/queries/QueryableStorage.d.ts +20 -0
  209. package/dist/esm/queries/QueryableStorage.js +80 -0
  210. package/dist/esm/queries/QueryableStorage.js.map +1 -0
  211. package/dist/esm/queries/dbQueries.js +13 -3
  212. package/dist/esm/queries/dbQueries.js.map +1 -1
  213. package/dist/esm/sync/FileSync.d.ts +1 -0
  214. package/dist/esm/sync/FileSync.js +1 -0
  215. package/dist/esm/sync/FileSync.js.map +1 -1
  216. package/dist/esm/sync/PushPullSync.d.ts +2 -1
  217. package/dist/esm/sync/PushPullSync.js +7 -1
  218. package/dist/esm/sync/PushPullSync.js.map +1 -1
  219. package/dist/esm/sync/Sync.d.ts +6 -3
  220. package/dist/esm/sync/Sync.js +9 -4
  221. package/dist/esm/sync/Sync.js.map +1 -1
  222. package/dist/esm/sync/WebSocketSync.d.ts +4 -1
  223. package/dist/esm/sync/WebSocketSync.js +41 -11
  224. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  225. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  226. package/dist/tsconfig.tsbuildinfo +1 -1
  227. package/package.json +8 -7
  228. package/src/DocumentManager.ts +1 -1
  229. package/src/IDBService.ts +78 -17
  230. package/src/UndoHistory.ts +5 -3
  231. package/src/__tests__/batching.test.ts +5 -2
  232. package/src/__tests__/documents.test.ts +44 -6
  233. package/src/__tests__/fixtures/testStorage.ts +3 -0
  234. package/src/__tests__/legacyOids.test.ts +53 -17
  235. package/src/__tests__/mutations.test.ts +9 -3
  236. package/src/__tests__/queries.test.ts +6 -2
  237. package/src/__tests__/setup/indexedDB.ts +8 -1
  238. package/src/__tests__/undo.test.ts +17 -9
  239. package/src/client/Client.ts +7 -3
  240. package/src/client/ClientDescriptor.ts +24 -8
  241. package/src/context.ts +16 -1
  242. package/src/entities/Entity.test.ts +218 -0
  243. package/src/entities/Entity.ts +696 -616
  244. package/src/entities/EntityCache.ts +41 -0
  245. package/src/entities/EntityMetadata.ts +364 -0
  246. package/src/entities/EntityStore.ts +384 -621
  247. package/src/entities/OperationBatcher.ts +251 -0
  248. package/src/entities/types.ts +154 -0
  249. package/src/files/EntityFile.ts +9 -4
  250. package/src/files/FileManager.ts +5 -3
  251. package/src/files/FileStorage.ts +7 -13
  252. package/src/files/utils.ts +6 -2
  253. package/src/idb.ts +51 -3
  254. package/src/index.ts +1 -1
  255. package/src/metadata/AckInfoStore.ts +1 -1
  256. package/src/metadata/BaselinesStore.ts +16 -24
  257. package/src/metadata/LocalReplicaStore.ts +13 -6
  258. package/src/metadata/Metadata.ts +109 -24
  259. package/src/metadata/OperationsStore.ts +37 -16
  260. package/src/migration/openDatabase.ts +32 -10
  261. package/src/queries/BaseQuery.ts +14 -1
  262. package/src/queries/CollectionQueries.ts +1 -1
  263. package/src/queries/FindAllQuery.ts +4 -0
  264. package/src/queries/QueryCache.ts +5 -0
  265. package/src/queries/QueryableStorage.ts +107 -0
  266. package/src/queries/dbQueries.ts +10 -3
  267. package/src/sync/FileSync.ts +2 -0
  268. package/src/sync/PushPullSync.ts +8 -1
  269. package/src/sync/Sync.ts +14 -6
  270. package/src/sync/WebSocketSync.ts +47 -10
  271. package/dist/cjs/entities/DocumentFamiliyCache.d.ts +0 -96
  272. package/dist/cjs/entities/DocumentFamiliyCache.js +0 -287
  273. package/dist/cjs/entities/DocumentFamiliyCache.js.map +0 -1
  274. package/dist/esm/entities/DocumentFamiliyCache.d.ts +0 -96
  275. package/dist/esm/entities/DocumentFamiliyCache.js +0 -283
  276. package/dist/esm/entities/DocumentFamiliyCache.js.map +0 -1
  277. package/src/entities/DocumentFamiliyCache.ts +0 -426
  278. package/src/entities/design.tldr +0 -808
@@ -0,0 +1,251 @@
1
+ import {
2
+ Batcher,
3
+ Operation,
4
+ generateId,
5
+ getOidRoot,
6
+ getUndoOperations,
7
+ groupPatchesByOid,
8
+ } from '@verdant-web/common';
9
+ import { Metadata } from '../metadata/Metadata.js';
10
+ import { Context } from '../context.js';
11
+ import type { EntityStore } from './EntityStore.js';
12
+ import { Entity } from './Entity.js';
13
+
14
+ const DEFAULT_BATCH_KEY = '@@default';
15
+
16
+ export interface OperationBatch {
17
+ run: (fn: () => void) => this;
18
+ /** @deprecated - use commit() */
19
+ flush: () => Promise<void>;
20
+ commit: () => Promise<void>;
21
+ discard: () => void;
22
+ }
23
+
24
+ export class OperationBatcher {
25
+ private batcher;
26
+ private currentBatchKey = DEFAULT_BATCH_KEY;
27
+ private defaultBatchTimeout: number;
28
+ private meta;
29
+ private ctx;
30
+ private entities;
31
+
32
+ constructor({
33
+ batchTimeout = 200,
34
+ meta,
35
+ ctx,
36
+ entities,
37
+ }: {
38
+ batchTimeout?: number;
39
+ meta: Metadata;
40
+ ctx: Context;
41
+ entities: EntityStore;
42
+ }) {
43
+ this.meta = meta;
44
+ this.ctx = ctx;
45
+ this.entities = entities;
46
+ this.defaultBatchTimeout = batchTimeout;
47
+ this.batcher = new Batcher<Operation, { undoable?: boolean }>(
48
+ this.flushOperations,
49
+ );
50
+ this.batcher.add({
51
+ key: DEFAULT_BATCH_KEY,
52
+ items: [],
53
+ max: 100,
54
+ timeout: batchTimeout,
55
+ userData: { undoable: true },
56
+ });
57
+ }
58
+
59
+ get isDefaultBatch() {
60
+ return this.currentBatchKey === DEFAULT_BATCH_KEY;
61
+ }
62
+
63
+ private flushOperations = async (
64
+ operations: Operation[],
65
+ batchKey: string,
66
+ meta: { undoable?: boolean },
67
+ ) => {
68
+ this.ctx.log(
69
+ 'debug',
70
+ 'Flushing',
71
+ operations.length,
72
+ 'operations from batch',
73
+ batchKey,
74
+ 'to storage / sync',
75
+ );
76
+ if (!operations.length) return;
77
+ // rewrite timestamps of all operations to now - this preserves
78
+ // the linear history of operations which are sent to the server.
79
+ // even if multiple batches are spun up in parallel and flushed
80
+ // after delay, the final operations in each one should reflect
81
+ // when the batch flushed, not when the changes were made.
82
+ // This also corresponds to user-observed behavior, since unconfirmed
83
+ // operations are applied universally after confirmed operations locally,
84
+ // so even operations which were made before a remote operation but
85
+ // have not been confirmed yet will appear to come after the remote one
86
+ // despite the provisional timestamp being earlier
87
+ // NOTE: this MUST be mutating the original operation object! this timestamp
88
+ // also serves as a unique ID for deduplication later.
89
+ for (const op of operations) {
90
+ op.timestamp = this.meta.now;
91
+ }
92
+ await this.commitOperations(operations, meta);
93
+ };
94
+
95
+ /**
96
+ * Immediately flushes operations to storage / sync.
97
+ * Providing source to second arg skips hydrating related
98
+ * Entity from storage, which is useful when that Entity
99
+ * isn't in storage (i.e. still creating) or just to speed
100
+ * up the commit.
101
+ */
102
+ commitOperations = async (
103
+ operations: Operation[],
104
+ meta: { undoable?: boolean; source?: Entity },
105
+ ) => {
106
+ if (!operations.length) return;
107
+ // now is the time to decide on what the undo operations will
108
+ // look like, based on the confirmed view of the related entities.
109
+ if (meta.undoable) {
110
+ const undo = await this.createUndo({
111
+ ops: operations,
112
+ source: meta.source,
113
+ });
114
+ if (undo) this.ctx.undoHistory.addUndo(undo);
115
+ }
116
+ // ship it out to EntityStore to compute final snapshots
117
+ // write to storage and refresh entities and queries
118
+ await this.entities.addData({
119
+ operations,
120
+ baselines: [],
121
+ isLocal: true,
122
+ });
123
+ };
124
+
125
+ /**
126
+ * Adds operations to the active batch.
127
+ */
128
+ addOperations = (operations: Operation[]) => {
129
+ if (!operations.length) return;
130
+ this.batcher.add({
131
+ key: this.currentBatchKey,
132
+ items: operations,
133
+ });
134
+ this.ctx.log(
135
+ `debug`,
136
+ 'added',
137
+ operations.length,
138
+ 'ops to batch',
139
+ this.currentBatchKey,
140
+ ', size = ',
141
+ this.batcher.getSize(this.currentBatchKey),
142
+ );
143
+ };
144
+
145
+ batch = ({
146
+ undoable = true,
147
+ batchName = generateId(),
148
+ max = null,
149
+ timeout = this.defaultBatchTimeout,
150
+ }: {
151
+ undoable?: boolean;
152
+ batchName?: string;
153
+ max?: number | null;
154
+ timeout?: number | null;
155
+ } = {}): OperationBatch => {
156
+ const internalBatch = this.batcher.add({
157
+ key: batchName,
158
+ max,
159
+ timeout,
160
+ items: [],
161
+ userData: { undoable },
162
+ });
163
+ const externalApi = {
164
+ run: (fn: () => void) => {
165
+ // while the provided function runs, operations are forwarded
166
+ // to the new batch instead of default. this relies on the function
167
+ // being synchronous.
168
+ this.currentBatchKey = batchName;
169
+ fn();
170
+ this.currentBatchKey = DEFAULT_BATCH_KEY;
171
+ return externalApi;
172
+ },
173
+ commit: async () => {
174
+ // before running a batch, the default operations must be flushed
175
+ // this better preserves undo history behavior...
176
+ // if we left the default batch open while flushing a named batch,
177
+ // then the default batch would be flushed after the named batch,
178
+ // and the default batch could contain operations both prior and
179
+ // after the named batch. this would result in a confusing undo
180
+ // history where the first undo might reverse changes before and
181
+ // after a set of other changes.
182
+ await this.batcher.flush(DEFAULT_BATCH_KEY);
183
+ return internalBatch.flush();
184
+ },
185
+ flush: () => externalApi.commit(),
186
+ discard: () => {
187
+ this.batcher.discard(batchName);
188
+ },
189
+ };
190
+ return externalApi;
191
+ };
192
+
193
+ flushAll = () => Promise.all(this.batcher.flushAll());
194
+
195
+ private createUndo = async (data: { ops: Operation[]; source?: Entity }) => {
196
+ // this can't be done on-demand because we rely on the current
197
+ // state of the entities to calculate the inverse operations.
198
+ const inverseOps = await this.getInverseOperations(data);
199
+
200
+ if (!inverseOps.length) return null;
201
+
202
+ return async () => {
203
+ const redo = await this.createUndo({
204
+ ops: inverseOps,
205
+ source: data.source,
206
+ });
207
+ // set time to now for all undo operations, they're happening now.
208
+ for (const op of inverseOps) {
209
+ op.timestamp = this.meta.now;
210
+ }
211
+ await this.commitOperations(
212
+ inverseOps,
213
+ // undos should not generate their own undo operations
214
+ // since they already calculate redo as the inverse.
215
+ { undoable: false },
216
+ );
217
+ return redo;
218
+ };
219
+ };
220
+ private getInverseOperations = async ({
221
+ ops,
222
+ source,
223
+ }: {
224
+ ops: Operation[];
225
+ source?: Entity;
226
+ }) => {
227
+ const grouped = groupPatchesByOid(ops);
228
+ const inverseOps: Operation[] = [];
229
+ const getNow = () => this.meta.now;
230
+ await Promise.all(
231
+ Object.entries(grouped).map(async ([oid, patches]): Promise<void> => {
232
+ const entity = source ?? (await this.entities.hydrate(getOidRoot(oid)));
233
+ // TODO: this is getting the rebased baseline? how? are ops being submitted early?
234
+ const viewData = entity?.__getViewData__(oid, 'confirmed');
235
+ if (!viewData) {
236
+ this.ctx.log(
237
+ 'warn',
238
+ 'could not find entity',
239
+ oid,
240
+ 'for undo operation',
241
+ ops,
242
+ );
243
+ return;
244
+ }
245
+ const inverse = getUndoOperations(oid, viewData.view, patches, getNow);
246
+ inverseOps.unshift(...inverse);
247
+ }),
248
+ );
249
+ return inverseOps;
250
+ };
251
+ }
@@ -0,0 +1,154 @@
1
+ import { ObjectIdentifier } from '@verdant-web/common';
2
+ import type { Entity } from './Entity.js';
3
+
4
+ export type AccessibleEntityProperty<T> = T extends Array<any>
5
+ ? number
6
+ : T extends object
7
+ ? keyof T
8
+ : never;
9
+
10
+ export type DataFromInit<Init> = Init extends { [key: string]: any }
11
+ ? {
12
+ [Key in keyof Init]: Init[Key];
13
+ }
14
+ : Init extends Array<any>
15
+ ? Init
16
+ : any;
17
+
18
+ // reduces keys of an object to only ones with an optional
19
+ // value
20
+ export type DeletableKeys<T> = keyof {
21
+ [Key in keyof T as IfNullableThen<T[Key], Key>]: Key;
22
+ };
23
+ type IfNullableThen<T, Out> = undefined extends T
24
+ ? Out
25
+ : null extends T
26
+ ? Out
27
+ : never;
28
+
29
+ export type EntityShape<E extends Entity<any, any>> = E extends Entity<
30
+ infer Value,
31
+ any
32
+ >
33
+ ? Value
34
+ : never;
35
+
36
+ export type BaseEntityValue = { [Key: string]: any } | any[];
37
+
38
+ export interface EntityChange {
39
+ oid: ObjectIdentifier;
40
+ isLocal: boolean;
41
+ }
42
+
43
+ export interface EntityChangeInfo {
44
+ isLocal?: boolean;
45
+ }
46
+
47
+ export type EntityEvents = {
48
+ change: (info: EntityChangeInfo) => void;
49
+ changeDeep: (
50
+ target: BaseEntity<any, any, any>,
51
+ info: EntityChangeInfo,
52
+ ) => void;
53
+ delete: (info: EntityChangeInfo) => void;
54
+ restore: (info: EntityChangeInfo) => void;
55
+ };
56
+
57
+ export interface BaseEntity<
58
+ Init,
59
+ Value extends BaseEntityValue,
60
+ Snapshot = DataFromInit<Init>,
61
+ > {
62
+ dispose: () => void;
63
+ subscribe<EventName extends keyof EntityEvents>(
64
+ event: EventName,
65
+ callback: EntityEvents[EventName],
66
+ ): () => void;
67
+ get<Key extends keyof Value>(key: Key): Value[Key];
68
+ getAll(): Value;
69
+ getSnapshot(): Snapshot;
70
+ readonly deleted: boolean;
71
+ readonly updatedAt: number;
72
+ readonly uid: string;
73
+ }
74
+
75
+ export type DeepPartial<T> = {
76
+ [P in keyof T]?: T[P] extends Array<infer U>
77
+ ? Array<DeepPartial<U>>
78
+ : T[P] extends ReadonlyArray<infer U>
79
+ ? ReadonlyArray<DeepPartial<U>>
80
+ : DeepPartial<T[P]>;
81
+ };
82
+
83
+ export interface ObjectEntity<
84
+ Init,
85
+ Value extends BaseEntityValue,
86
+ Snapshot = DataFromInit<Init>,
87
+ > extends BaseEntity<Init, Value, Snapshot> {
88
+ keys(): string[];
89
+ entries(): [string, Exclude<Value[keyof Value], undefined>][];
90
+ values(): Exclude<Value[keyof Value], undefined>[];
91
+ set<Key extends keyof Init>(key: Key, value: Init[Key]): void;
92
+ delete(key: DeletableKeys<Value>): void;
93
+ update(
94
+ value: DeepPartial<Init>,
95
+ options?: { replaceSubObjects?: boolean; merge?: boolean },
96
+ ): void;
97
+ readonly isList: false;
98
+ }
99
+
100
+ export interface ListEntity<
101
+ Init,
102
+ Value extends BaseEntityValue,
103
+ Snapshot = DataFromInit<Init>,
104
+ > extends Iterable<ListItemValue<Value>>,
105
+ BaseEntity<Init, Value, Snapshot> {
106
+ readonly isList: true;
107
+ readonly length: number;
108
+ push(value: ListItemInit<Init>): void;
109
+ insert(index: number, value: ListItemInit<Init>): void;
110
+ move(from: number, to: number): void;
111
+ moveItem(item: ListItemValue<Value>, to: number): void;
112
+ /**
113
+ * A Set operation which adds a value if an equivalent value is not already present.
114
+ * Object values are never the same.
115
+ */
116
+ add(value: ListItemValue<Value>): void;
117
+ removeAll(item: ListItemValue<Value>): void;
118
+ removeFirst(item: ListItemValue<Value>): void;
119
+ removeLast(item: ListItemValue<Value>): void;
120
+ map<U>(callback: (value: ListItemValue<Value>, index: number) => U): U[];
121
+ filter(
122
+ callback: (value: ListItemValue<Value>, index: number) => boolean,
123
+ ): ListItemValue<Value>[];
124
+ delete(index: number): void;
125
+ has(value: ListItemValue<Value>): boolean;
126
+ forEach(callback: (value: ListItemValue<Value>, index: number) => void): void;
127
+ some(predicate: (value: ListItemValue<Value>) => boolean): boolean;
128
+ every(predicate: (value: ListItemValue<Value>) => boolean): boolean;
129
+ find(
130
+ predicate: (value: ListItemValue<Value>) => boolean,
131
+ ): ListItemValue<Value> | undefined;
132
+ includes(value: ListItemValue<Value>): boolean;
133
+ }
134
+
135
+ export type AnyEntity<
136
+ Init,
137
+ KeyValue extends BaseEntityValue,
138
+ Snapshot extends any,
139
+ > =
140
+ | ListEntity<Init, KeyValue, Snapshot>
141
+ | ObjectEntity<Init, KeyValue, Snapshot>;
142
+
143
+ export type ListItemValue<KeyValue> = KeyValue extends Array<infer T>
144
+ ? T
145
+ : never;
146
+ export type ListItemInit<Init> = Init extends Array<infer T> ? T : never;
147
+
148
+ export type EntityDestructured<T extends AnyEntity<any, any, any> | null> =
149
+ | (T extends ListEntity<any, infer KeyValue, any>
150
+ ? KeyValue
151
+ : T extends ObjectEntity<any, infer KeyValue, any>
152
+ ? KeyValue
153
+ : never)
154
+ | (T extends null ? null : never);
@@ -17,7 +17,6 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
17
17
  private _fileData: FileData | null = null;
18
18
  private _loading = true;
19
19
  private _failed = false;
20
- private _disposed = false;
21
20
  private _downloadRemote = false;
22
21
 
23
22
  constructor(
@@ -39,7 +38,6 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
39
38
  [UPDATE] = (fileData: FileData) => {
40
39
  this._loading = false;
41
40
  this._failed = false;
42
- this._disposed = false;
43
41
  this._fileData = fileData;
44
42
  if (fileData.file) {
45
43
  if (this._objectUrl) {
@@ -78,10 +76,17 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
78
76
  return this._failed;
79
77
  }
80
78
 
81
- dispose = () => {
79
+ destroy = () => {
82
80
  if (this._objectUrl) {
83
81
  URL.revokeObjectURL(this._objectUrl);
84
82
  }
85
- this._disposed = true;
83
+ this.dispose();
86
84
  };
85
+
86
+ getSnapshot() {
87
+ return {
88
+ id: this.id,
89
+ url: this.loading || this.failed ? undefined : this.url,
90
+ };
91
+ }
87
92
  }
@@ -108,11 +108,11 @@ export class FileManager {
108
108
  * Immediately returns an EntityFile to use, then either loads
109
109
  * the file from cache, local database, or the server.
110
110
  */
111
- get = (id: string) => {
111
+ get = (id: string, options?: { downloadRemote?: boolean }) => {
112
112
  if (this.files.has(id)) {
113
113
  return this.files.get(id)!;
114
114
  }
115
- const file = new EntityFile(id);
115
+ const file = new EntityFile(id, options);
116
116
  this.files.set(id, file);
117
117
  this.load(file);
118
118
  return file;
@@ -120,6 +120,7 @@ export class FileManager {
120
120
 
121
121
  private load = async (file: EntityFile, retries = 0) => {
122
122
  if (retries > 5) {
123
+ this.context.log('error', 'Failed to load file after 5 retries');
123
124
  file[MARK_FAILED]();
124
125
  return;
125
126
  }
@@ -137,6 +138,7 @@ export class FileManager {
137
138
  downloadRemote: file.downloadRemote,
138
139
  });
139
140
  } else {
141
+ this.context.log('error', 'Failed to load file', result);
140
142
  file[MARK_FAILED]();
141
143
  if (result.retry) {
142
144
  // schedule a retry
@@ -183,7 +185,7 @@ export class FileManager {
183
185
  };
184
186
 
185
187
  private handleFileRefsDeleted = async (fileRefs: FileRef[]) => {
186
- const tx = this.storage.createTransaction(['files'], 'readwrite');
188
+ const tx = this.storage.createTransaction(['files'], { mode: 'readwrite' });
187
189
  await Promise.all(
188
190
  fileRefs.map(async (fileRef) => {
189
191
  try {
@@ -53,8 +53,7 @@ export class FileStorage extends IDBService {
53
53
  buffer,
54
54
  } as StoredFileData);
55
55
  },
56
- 'readwrite',
57
- transaction,
56
+ { mode: 'readwrite', transaction },
58
57
  );
59
58
  };
60
59
 
@@ -86,8 +85,7 @@ export class FileStorage extends IDBService {
86
85
  remote: 'true',
87
86
  } as StoredFileData);
88
87
  },
89
- 'readwrite',
90
- transaction,
88
+ { mode: 'readwrite', transaction },
91
89
  );
92
90
  };
93
91
 
@@ -100,8 +98,7 @@ export class FileStorage extends IDBService {
100
98
  (store) => {
101
99
  return store.get(id);
102
100
  },
103
- 'readonly',
104
- transaction,
101
+ { mode: 'readonly', transaction },
105
102
  );
106
103
  if (!raw) {
107
104
  return undefined;
@@ -129,8 +126,7 @@ export class FileStorage extends IDBService {
129
126
  (store) => {
130
127
  return store.delete(id);
131
128
  },
132
- 'readwrite',
133
- transaction,
129
+ { mode: 'readwrite', transaction },
134
130
  );
135
131
  }
136
132
 
@@ -152,8 +148,7 @@ export class FileStorage extends IDBService {
152
148
  deletedAt: Date.now(),
153
149
  } as StoredFileData);
154
150
  },
155
- 'readwrite',
156
- transaction,
151
+ { mode: 'readwrite', transaction },
157
152
  );
158
153
  };
159
154
 
@@ -163,7 +158,7 @@ export class FileStorage extends IDBService {
163
158
  (store) => {
164
159
  return store.index('remote').getAll('false');
165
160
  },
166
- 'readonly',
161
+ { mode: 'readonly' },
167
162
  );
168
163
  return raw.map(this.hydrateFileData);
169
164
  };
@@ -182,8 +177,7 @@ export class FileStorage extends IDBService {
182
177
  (value, store) => {
183
178
  iterator(this.hydrateFileData(value), store);
184
179
  },
185
- 'readwrite',
186
- transaction,
180
+ { mode: 'readwrite', transaction },
187
181
  );
188
182
  };
189
183
  }
@@ -12,11 +12,15 @@ export function createFileData(file: File): FileData {
12
12
  };
13
13
  }
14
14
 
15
- function isFile(value: any): value is File {
16
- return value instanceof File;
15
+ export function isFile(value: any): value is File {
16
+ return (
17
+ value instanceof File ||
18
+ (typeof Blob !== 'undefined' && value instanceof Blob)
19
+ );
17
20
  }
18
21
 
19
22
  /**
23
+ * MUTATES the value.
20
24
  * Replaces File values with refs and returns the normalized value.
21
25
  * The list of files passed to the second argument will be populated with the files found in the value.
22
26
  */
package/src/idb.ts CHANGED
@@ -1,12 +1,21 @@
1
1
  import { roughSizeOfObject } from '@verdant-web/common';
2
2
 
3
+ export function isAbortError(err: unknown) {
4
+ return err instanceof Error && err.name === 'AbortError';
5
+ }
6
+
3
7
  export function storeRequestPromise<T>(request: IDBRequest<T>) {
4
8
  return new Promise<T>((resolve, reject) => {
5
9
  request.onsuccess = () => {
6
10
  resolve(request.result);
7
11
  };
8
12
  request.onerror = () => {
9
- reject(request.error);
13
+ if (request.error && isAbortError(request.error)) {
14
+ // TODO: is this the right thing to do?
15
+ resolve(request.result);
16
+ } else {
17
+ reject(request.error);
18
+ }
10
19
  };
11
20
  });
12
21
  }
@@ -29,7 +38,11 @@ export function cursorIterator<T>(
29
38
  }
30
39
  };
31
40
  request.onerror = () => {
32
- reject(request.error);
41
+ if (request.error && isAbortError(request.error)) {
42
+ resolve();
43
+ } else {
44
+ reject(request.error);
45
+ }
33
46
  };
34
47
  });
35
48
  }
@@ -53,7 +66,14 @@ export function getSizeOfObjectStore(
53
66
  }
54
67
  };
55
68
  cursorReq.onerror = function (e) {
56
- reject(e);
69
+ if (cursorReq.error && isAbortError(cursorReq.error)) {
70
+ resolve({
71
+ count: count,
72
+ size: size,
73
+ });
74
+ } else {
75
+ reject(cursorReq.error);
76
+ }
57
77
  };
58
78
  tx.oncomplete = function (e) {
59
79
  resolve({
@@ -115,3 +135,31 @@ export async function getAllDatabaseNamesAndVersions(
115
135
  ) {
116
136
  return indexedDB.databases();
117
137
  }
138
+
139
+ export function createAbortableTransaction(
140
+ db: IDBDatabase,
141
+ storeNames: string[],
142
+ mode: 'readonly' | 'readwrite',
143
+ abortSignal?: AbortSignal,
144
+ log?: (...args: any[]) => void,
145
+ ) {
146
+ const tx = db.transaction(storeNames, mode);
147
+ if (abortSignal) {
148
+ const abort = () => {
149
+ log?.('debug', 'aborting transaction');
150
+ try {
151
+ tx.abort();
152
+ } catch (e) {
153
+ log?.('debug', 'aborting transaction failed', e);
154
+ }
155
+ };
156
+ abortSignal.addEventListener('abort', abort);
157
+ tx.addEventListener('error', () => {
158
+ abortSignal.removeEventListener('abort', abort);
159
+ });
160
+ tx.addEventListener('complete', () => {
161
+ abortSignal.removeEventListener('abort', abort);
162
+ });
163
+ }
164
+ return tx;
165
+ }
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ export type {
19
19
  AccessibleEntityProperty,
20
20
  AnyEntity,
21
21
  EntityDestructured,
22
- } from './entities/Entity.js';
22
+ } from './entities/types.js';
23
23
  export { ServerSync } from './sync/Sync.js';
24
24
  export type { SyncTransportMode } from './sync/Sync.js';
25
25
  export { EntityFile } from './files/EntityFile.js';
@@ -24,7 +24,7 @@ export class AckInfoStore extends IDBService {
24
24
  await this.run(
25
25
  'info',
26
26
  (store) => store.put({ type: 'ack', globalAckTimestamp: ack }),
27
- 'readwrite',
27
+ { mode: 'readwrite' },
28
28
  );
29
29
  };
30
30
  }