@tldraw/store 4.3.0-canary.da35795ba8e2 → 4.3.0-canary.e1766dd4eab3

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.
@@ -0,0 +1,20 @@
1
+ let _isDev = false
2
+ try {
3
+ _isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'
4
+ } catch (_e) {
5
+ /* noop */
6
+ }
7
+ try {
8
+ _isDev =
9
+ _isDev ||
10
+ (import.meta as any).env.DEV ||
11
+ (import.meta as any).env.TEST ||
12
+ (import.meta as any).env.MODE === 'development' ||
13
+ (import.meta as any).env.MODE === 'test'
14
+ } catch (_e) {
15
+ /* noop */
16
+ }
17
+
18
+ export function isDev() {
19
+ return _isDev
20
+ }
@@ -1,6 +1,7 @@
1
1
  import { assert, objectMapEntries } from '@tldraw/utils'
2
2
  import { UnknownRecord } from './BaseRecord'
3
3
  import { SerializedStore } from './Store'
4
+ import { SerializedSchema } from './StoreSchema'
4
5
 
5
6
  function squashDependsOn(sequence: Array<Migration | StandaloneDependsOn>): Migration[] {
6
7
  const result: Migration[] = []
@@ -219,8 +220,36 @@ export type Migration = {
219
220
  newState: SerializedStore<UnknownRecord>
220
221
  ) => void | SerializedStore<UnknownRecord>
221
222
  }
223
+ | {
224
+ readonly scope: 'storage'
225
+ // eslint-disable-next-line @typescript-eslint/method-signature-style
226
+ readonly up: (storage: SynchronousRecordStorage<UnknownRecord>) => void
227
+ readonly down?: never
228
+ }
222
229
  )
223
230
 
231
+ /**
232
+ * Abstraction over the store that can be used to perform migrations.
233
+ * @public
234
+ */
235
+ export interface SynchronousRecordStorage<R extends UnknownRecord> {
236
+ get(id: string): R | undefined
237
+ set(id: string, record: R): void
238
+ delete(id: string): void
239
+ keys(): Iterable<string>
240
+ values(): Iterable<R>
241
+ entries(): Iterable<[string, R]>
242
+ }
243
+
244
+ /**
245
+ * Abstraction over the storage that can be used to perform migrations.
246
+ * @public
247
+ */
248
+ export interface SynchronousStorage<R extends UnknownRecord> extends SynchronousRecordStorage<R> {
249
+ getSchema(): SerializedSchema
250
+ setSchema(schema: SerializedSchema): void
251
+ }
252
+
224
253
  /**
225
254
  * Base interface for legacy migration information.
226
255
  *
@@ -883,6 +883,188 @@ describe('snapshots', () => {
883
883
  expect(up).toHaveBeenCalledTimes(1)
884
884
  expect(store2.get(Book.createId('lotr'))!.numPages).toBe(42)
885
885
  })
886
+
887
+ it('migrates the snapshot with storage scope', () => {
888
+ const snapshot1 = store.getStoreSnapshot()
889
+ const up = vi.fn((storage: any) => {
890
+ const book = storage.get('book:lotr')
891
+ storage.set('book:lotr', { ...book, numPages: 42 })
892
+ })
893
+
894
+ expect((snapshot1.store as any)['book:lotr'].numPages).toBe(1000)
895
+
896
+ const store2 = new Store({
897
+ props: {},
898
+ schema: StoreSchema.create<Book | Author>(
899
+ {
900
+ book: Book,
901
+ author: Author,
902
+ },
903
+ {
904
+ migrations: [
905
+ createMigrationSequence({
906
+ sequenceId: 'com.tldraw',
907
+ retroactive: true,
908
+ sequence: [
909
+ {
910
+ id: `com.tldraw/1`,
911
+ scope: 'storage',
912
+ up,
913
+ },
914
+ ],
915
+ }),
916
+ ],
917
+ }
918
+ ),
919
+ })
920
+
921
+ expect(() => {
922
+ store2.loadStoreSnapshot(snapshot1)
923
+ }).not.toThrow()
924
+
925
+ expect(up).toHaveBeenCalledTimes(1)
926
+ expect(store2.get(Book.createId('lotr'))!.numPages).toBe(42)
927
+ })
928
+
929
+ it('storage scope migration can delete records', () => {
930
+ const snapshot1 = store.getStoreSnapshot()
931
+ const up = vi.fn((storage: any) => {
932
+ storage.delete('author:mcavoy')
933
+ })
934
+
935
+ expect((snapshot1.store as any)['author:mcavoy']).toBeDefined()
936
+
937
+ const store2 = new Store({
938
+ props: {},
939
+ schema: StoreSchema.create<Book | Author>(
940
+ {
941
+ book: Book,
942
+ author: Author,
943
+ },
944
+ {
945
+ migrations: [
946
+ createMigrationSequence({
947
+ sequenceId: 'com.tldraw',
948
+ retroactive: true,
949
+ sequence: [
950
+ {
951
+ id: `com.tldraw/1`,
952
+ scope: 'storage',
953
+ up,
954
+ },
955
+ ],
956
+ }),
957
+ ],
958
+ }
959
+ ),
960
+ })
961
+
962
+ expect(() => {
963
+ store2.loadStoreSnapshot(snapshot1)
964
+ }).not.toThrow()
965
+
966
+ expect(up).toHaveBeenCalledTimes(1)
967
+ expect(store2.get(Author.createId('mcavoy'))).toBeUndefined()
968
+ })
969
+
970
+ it('storage scope migration can iterate records', () => {
971
+ const snapshot1 = store.getStoreSnapshot()
972
+ const up = vi.fn((storage: any) => {
973
+ for (const [id, record] of storage.entries()) {
974
+ if (record.typeName === 'book') {
975
+ storage.set(id, { ...record, numPages: record.numPages + 100 })
976
+ }
977
+ }
978
+ })
979
+
980
+ expect((snapshot1.store as any)['book:lotr'].numPages).toBe(1000)
981
+ expect((snapshot1.store as any)['book:hobbit'].numPages).toBe(300)
982
+
983
+ const store2 = new Store({
984
+ props: {},
985
+ schema: StoreSchema.create<Book | Author>(
986
+ {
987
+ book: Book,
988
+ author: Author,
989
+ },
990
+ {
991
+ migrations: [
992
+ createMigrationSequence({
993
+ sequenceId: 'com.tldraw',
994
+ retroactive: true,
995
+ sequence: [
996
+ {
997
+ id: `com.tldraw/1`,
998
+ scope: 'storage',
999
+ up,
1000
+ },
1001
+ ],
1002
+ }),
1003
+ ],
1004
+ }
1005
+ ),
1006
+ })
1007
+
1008
+ expect(() => {
1009
+ store2.loadStoreSnapshot(snapshot1)
1010
+ }).not.toThrow()
1011
+
1012
+ expect(up).toHaveBeenCalledTimes(1)
1013
+ expect(store2.get(Book.createId('lotr'))!.numPages).toBe(1100)
1014
+ expect(store2.get(Book.createId('hobbit'))!.numPages).toBe(400)
1015
+ })
1016
+
1017
+ it('storage scope migration can use values() and keys()', () => {
1018
+ const snapshot1 = store.getStoreSnapshot()
1019
+ const keysCollected: string[] = []
1020
+ const valuesCollected: any[] = []
1021
+
1022
+ const up = vi.fn((storage: any) => {
1023
+ for (const key of storage.keys()) {
1024
+ keysCollected.push(key)
1025
+ }
1026
+ for (const value of storage.values()) {
1027
+ valuesCollected.push(value)
1028
+ }
1029
+ })
1030
+
1031
+ const store2 = new Store({
1032
+ props: {},
1033
+ schema: StoreSchema.create<Book | Author>(
1034
+ {
1035
+ book: Book,
1036
+ author: Author,
1037
+ },
1038
+ {
1039
+ migrations: [
1040
+ createMigrationSequence({
1041
+ sequenceId: 'com.tldraw',
1042
+ retroactive: true,
1043
+ sequence: [
1044
+ {
1045
+ id: `com.tldraw/1`,
1046
+ scope: 'storage',
1047
+ up,
1048
+ },
1049
+ ],
1050
+ }),
1051
+ ],
1052
+ }
1053
+ ),
1054
+ })
1055
+
1056
+ expect(() => {
1057
+ store2.loadStoreSnapshot(snapshot1)
1058
+ }).not.toThrow()
1059
+
1060
+ expect(up).toHaveBeenCalledTimes(1)
1061
+ expect(keysCollected).toContain('book:lotr')
1062
+ expect(keysCollected).toContain('book:hobbit')
1063
+ expect(keysCollected).toContain('author:tolkein')
1064
+ expect(keysCollected).toContain('author:mcavoy')
1065
+ expect(keysCollected).toContain('author:cassidy')
1066
+ expect(valuesCollected.length).toBe(5)
1067
+ })
886
1068
  })
887
1069
 
888
1070
  describe('diffs', () => {