jazz-tools 0.19.12 → 0.19.13

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 (119) hide show
  1. package/.turbo/turbo-build.log +50 -50
  2. package/CHANGELOG.md +10 -0
  3. package/dist/browser/createBrowserContext.d.ts +1 -5
  4. package/dist/browser/createBrowserContext.d.ts.map +1 -1
  5. package/dist/browser/index.js +124 -47
  6. package/dist/browser/index.js.map +1 -1
  7. package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.d.ts +12 -0
  8. package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.d.ts.map +1 -0
  9. package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.test.d.ts +2 -0
  10. package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.test.d.ts.map +1 -0
  11. package/dist/browser/provideBrowserLockSession/SessionIDStorage.d.ts +6 -0
  12. package/dist/browser/provideBrowserLockSession/SessionIDStorage.d.ts.map +1 -0
  13. package/dist/browser/provideBrowserLockSession/index.d.ts +4 -0
  14. package/dist/browser/provideBrowserLockSession/index.d.ts.map +1 -0
  15. package/dist/{chunk-AGF4HEDH.js → chunk-GAPMDNJY.js} +437 -82
  16. package/dist/chunk-GAPMDNJY.js.map +1 -0
  17. package/dist/index.js +5 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/react-native/index.js +41 -12
  20. package/dist/react-native/index.js.map +1 -1
  21. package/dist/react-native-core/ReactNativeSessionProvider.d.ts +11 -0
  22. package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -0
  23. package/dist/react-native-core/index.js +41 -12
  24. package/dist/react-native-core/index.js.map +1 -1
  25. package/dist/react-native-core/platform.d.ts +2 -8
  26. package/dist/react-native-core/platform.d.ts.map +1 -1
  27. package/dist/react-native-core/tests/ReactNativeSessionProvider.test.d.ts +2 -0
  28. package/dist/react-native-core/tests/ReactNativeSessionProvider.test.d.ts.map +1 -0
  29. package/dist/testing.js +4 -3
  30. package/dist/testing.js.map +1 -1
  31. package/dist/tools/coValues/account.d.ts.map +1 -1
  32. package/dist/tools/coValues/coFeed.d.ts +2 -2
  33. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  34. package/dist/tools/coValues/coList.d.ts +1 -2
  35. package/dist/tools/coValues/coList.d.ts.map +1 -1
  36. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  37. package/dist/tools/coValues/coVector.d.ts.map +1 -1
  38. package/dist/tools/coValues/group.d.ts +5 -1
  39. package/dist/tools/coValues/group.d.ts.map +1 -1
  40. package/dist/tools/coValues/interfaces.d.ts +2 -1
  41. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  42. package/dist/tools/exports.d.ts +2 -2
  43. package/dist/tools/exports.d.ts.map +1 -1
  44. package/dist/tools/implementation/createContext.d.ts +21 -11
  45. package/dist/tools/implementation/createContext.d.ts.map +1 -1
  46. package/dist/tools/implementation/schema.d.ts +14 -6
  47. package/dist/tools/implementation/schema.d.ts.map +1 -1
  48. package/dist/tools/implementation/schemaUtils.d.ts +1 -1
  49. package/dist/tools/implementation/schemaUtils.d.ts.map +1 -1
  50. package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts.map +1 -1
  51. package/dist/tools/implementation/zodSchema/schemaPermissions.d.ts +99 -0
  52. package/dist/tools/implementation/zodSchema/schemaPermissions.d.ts.map +1 -0
  53. package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts +11 -0
  54. package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts.map +1 -1
  55. package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts +11 -0
  56. package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts.map +1 -1
  57. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +15 -1
  58. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  59. package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts +10 -0
  60. package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts.map +1 -1
  61. package/dist/tools/implementation/zodSchema/schemaTypes/CoVectorSchema.d.ts +9 -0
  62. package/dist/tools/implementation/zodSchema/schemaTypes/CoVectorSchema.d.ts.map +1 -1
  63. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +13 -1
  64. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
  65. package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts +10 -0
  66. package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts.map +1 -1
  67. package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts +6 -0
  68. package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts.map +1 -1
  69. package/dist/tools/implementation/zodSchema/unionUtils.d.ts +12 -1
  70. package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
  71. package/dist/tools/internal.d.ts +1 -0
  72. package/dist/tools/internal.d.ts.map +1 -1
  73. package/dist/tools/testing.d.ts.map +1 -1
  74. package/dist/tools/tests/schema.withPermissions.test.d.ts +2 -0
  75. package/dist/tools/tests/schema.withPermissions.test.d.ts.map +1 -0
  76. package/dist/worker/index.js +2 -2
  77. package/dist/worker/index.js.map +1 -1
  78. package/package.json +4 -4
  79. package/src/browser/createBrowserContext.ts +3 -62
  80. package/src/browser/provideBrowserLockSession/BrowserSessionProvider.test.ts +406 -0
  81. package/src/browser/provideBrowserLockSession/BrowserSessionProvider.ts +132 -0
  82. package/src/browser/provideBrowserLockSession/SessionIDStorage.ts +33 -0
  83. package/src/browser/provideBrowserLockSession/index.ts +11 -0
  84. package/src/react-native-core/ReactNativeSessionProvider.ts +52 -0
  85. package/src/react-native-core/platform.ts +5 -30
  86. package/src/react-native-core/tests/ReactNativeSessionProvider.test.ts +124 -0
  87. package/src/tools/coValues/account.ts +4 -0
  88. package/src/tools/coValues/coFeed.ts +8 -3
  89. package/src/tools/coValues/coList.ts +6 -3
  90. package/src/tools/coValues/coMap.ts +10 -0
  91. package/src/tools/coValues/coVector.ts +2 -1
  92. package/src/tools/coValues/group.ts +6 -4
  93. package/src/tools/coValues/interfaces.ts +19 -7
  94. package/src/tools/exports.ts +3 -1
  95. package/src/tools/implementation/createContext.ts +43 -15
  96. package/src/tools/implementation/schema.ts +23 -13
  97. package/src/tools/implementation/schemaUtils.ts +1 -1
  98. package/src/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.ts +105 -4
  99. package/src/tools/implementation/zodSchema/schemaPermissions.ts +188 -0
  100. package/src/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.ts +46 -3
  101. package/src/tools/implementation/zodSchema/schemaTypes/CoListSchema.ts +46 -3
  102. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +50 -13
  103. package/src/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.ts +14 -0
  104. package/src/tools/implementation/zodSchema/schemaTypes/CoVectorSchema.ts +24 -1
  105. package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +51 -4
  106. package/src/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.ts +25 -1
  107. package/src/tools/implementation/zodSchema/schemaTypes/RichTextSchema.ts +21 -1
  108. package/src/tools/implementation/zodSchema/unionUtils.ts +72 -20
  109. package/src/tools/internal.ts +1 -0
  110. package/src/tools/testing.ts +3 -1
  111. package/src/tools/tests/ContextManager.test.ts +2 -1
  112. package/src/tools/tests/coPlainText.test.ts +2 -2
  113. package/src/tools/tests/createContext.test.ts +79 -1
  114. package/src/tools/tests/deepLoading.test.ts +25 -2
  115. package/src/tools/tests/schema.resolved.test.ts +10 -0
  116. package/src/tools/tests/schema.withPermissions.test.ts +859 -0
  117. package/src/tools/tests/utils.ts +2 -2
  118. package/src/worker/index.ts +2 -2
  119. package/dist/chunk-AGF4HEDH.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/tools/testing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/tools/testing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema.withPermissions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.withPermissions.test.d.ts","sourceRoot":"","sources":["../../../src/tools/tests/schema.withPermissions.test.ts"],"names":[],"mappings":""}
@@ -7,7 +7,7 @@ import {
7
7
  Account,
8
8
  Inbox,
9
9
  createJazzContextFromExistingCredentials,
10
- randomSessionProvider
10
+ MockSessionProvider
11
11
  } from "jazz-tools";
12
12
  async function startWorker(options) {
13
13
  const {
@@ -53,7 +53,7 @@ async function startWorker(options) {
53
53
  secret: accountSecret
54
54
  },
55
55
  AccountSchema,
56
- sessionProvider: randomSessionProvider,
56
+ sessionProvider: new MockSessionProvider(),
57
57
  peers,
58
58
  crypto: options.crypto ?? await WasmCrypto.create(),
59
59
  asActiveAccount,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/worker/index.ts"],"sourcesContent":["import {\n AgentSecret,\n CryptoProvider,\n LocalNode,\n Peer,\n StorageAPI,\n} from \"cojson\";\nimport {\n type AnyWebSocketConstructor,\n WebSocketPeerWithReconnection,\n} from \"cojson-transport-ws\";\nimport { WasmCrypto } from \"cojson/crypto/WasmCrypto\";\nimport {\n Account,\n AccountClass,\n AnyAccountSchema,\n CoValueFromRaw,\n Inbox,\n InstanceOfSchema,\n Loaded,\n createJazzContextFromExistingCredentials,\n randomSessionProvider,\n} from \"jazz-tools\";\n\ntype WorkerOptions<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n> = {\n accountID?: string;\n accountSecret?: string;\n syncServer?: string;\n WebSocket?: AnyWebSocketConstructor;\n AccountSchema?: S;\n crypto?: CryptoProvider;\n /**\n * If true, the inbox will not be loaded.\n */\n skipInboxLoad?: boolean;\n /**\n * If false, the worker will not set in the global account context\n */\n asActiveAccount?: boolean;\n storage?: StorageAPI;\n};\n\n/** @category Context Creation */\nexport async function startWorker<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n>(options: WorkerOptions<S>) {\n const {\n accountID = process.env.JAZZ_WORKER_ACCOUNT,\n accountSecret = process.env.JAZZ_WORKER_SECRET,\n syncServer = \"wss://cloud.jazz.tools\",\n AccountSchema = Account as unknown as S,\n skipInboxLoad = false,\n asActiveAccount = true,\n } = options;\n\n let node: LocalNode | undefined = undefined;\n\n const peers: Peer[] = [];\n\n const wsPeer = new WebSocketPeerWithReconnection({\n peer: syncServer,\n reconnectionTimeout: 100,\n addPeer: (peer) => {\n if (node) {\n node.syncManager.addPeer(peer);\n } else {\n peers.push(peer);\n }\n },\n removePeer: () => {},\n WebSocketConstructor: options.WebSocket,\n });\n\n wsPeer.enable();\n\n if (!accountID) {\n throw new Error(\"No accountID provided\");\n }\n if (!accountSecret) {\n throw new Error(\"No accountSecret provided\");\n }\n if (!accountID.startsWith(\"co_\")) {\n throw new Error(\"Invalid accountID\");\n }\n if (!accountSecret?.startsWith(\"sealerSecret_\")) {\n throw new Error(\"Invalid accountSecret\");\n }\n\n const context = await createJazzContextFromExistingCredentials({\n credentials: {\n accountID: accountID,\n secret: accountSecret as AgentSecret,\n },\n AccountSchema,\n sessionProvider: randomSessionProvider,\n peers,\n crypto: options.crypto ?? (await WasmCrypto.create()),\n asActiveAccount,\n storage: options.storage,\n });\n\n const account = context.account as InstanceOfSchema<S>;\n node = account.$jazz.localNode;\n\n if (!account.$jazz.refs.profile?.id) {\n throw new Error(\"Account has no profile\");\n }\n\n const inbox = skipInboxLoad ? undefined : await Inbox.load(account);\n\n async function done() {\n await context.account.$jazz.waitForAllCoValuesSync();\n\n wsPeer.disable();\n context.done();\n }\n\n const inboxPublicApi = inbox\n ? {\n subscribe: inbox.subscribe.bind(inbox) as Inbox[\"subscribe\"],\n }\n : {\n subscribe: () => {},\n };\n\n return {\n /**\n * The worker account instance.\n */\n worker: context.account as Loaded<S>,\n experimental: {\n /**\n * API to subscribe to the inbox messages.\n *\n * More info on the Inbox API: https://jazz.tools/docs/react/server-side/inbox\n */\n inbox: inboxPublicApi,\n },\n /**\n * Wait for the connection to the sync server to be established.\n *\n * If already connected, it will resolve immediately.\n */\n waitForConnection() {\n return wsPeer.waitUntilConnected();\n },\n subscribeToConnectionChange(listener: (connected: boolean) => void) {\n wsPeer.subscribe(listener);\n\n return () => {\n wsPeer.unsubscribe(listener);\n };\n },\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n *\n * @deprecated Use shutdownWorker\n */\n done,\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n */\n shutdownWorker() {\n return done();\n },\n };\n}\n"],"mappings":";AAOA;AAAA,EAEE;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EAIA;AAAA,EAGA;AAAA,EACA;AAAA,OACK;AAyBP,eAAsB,YAIpB,SAA2B;AAC3B,QAAM;AAAA,IACJ,YAAY,QAAQ,IAAI;AAAA,IACxB,gBAAgB,QAAQ,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,EACpB,IAAI;AAEJ,MAAI,OAA8B;AAElC,QAAM,QAAgB,CAAC;AAEvB,QAAM,SAAS,IAAI,8BAA8B;AAAA,IAC/C,MAAM;AAAA,IACN,qBAAqB;AAAA,IACrB,SAAS,CAAC,SAAS;AACjB,UAAI,MAAM;AACR,aAAK,YAAY,QAAQ,IAAI;AAAA,MAC/B,OAAO;AACL,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,IACA,YAAY,MAAM;AAAA,IAAC;AAAA,IACnB,sBAAsB,QAAQ;AAAA,EAChC,CAAC;AAED,SAAO,OAAO;AAEd,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AACA,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AACA,MAAI,CAAC,UAAU,WAAW,KAAK,GAAG;AAChC,UAAM,IAAI,MAAM,mBAAmB;AAAA,EACrC;AACA,MAAI,CAAC,eAAe,WAAW,eAAe,GAAG;AAC/C,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAEA,QAAM,UAAU,MAAM,yCAAyC;AAAA,IAC7D,aAAa;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,IACjB;AAAA,IACA,QAAQ,QAAQ,UAAW,MAAM,WAAW,OAAO;AAAA,IACnD;AAAA,IACA,SAAS,QAAQ;AAAA,EACnB,CAAC;AAED,QAAM,UAAU,QAAQ;AACxB,SAAO,QAAQ,MAAM;AAErB,MAAI,CAAC,QAAQ,MAAM,KAAK,SAAS,IAAI;AACnC,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,QAAM,QAAQ,gBAAgB,SAAY,MAAM,MAAM,KAAK,OAAO;AAElE,iBAAe,OAAO;AACpB,UAAM,QAAQ,QAAQ,MAAM,uBAAuB;AAEnD,WAAO,QAAQ;AACf,YAAQ,KAAK;AAAA,EACf;AAEA,QAAM,iBAAiB,QACnB;AAAA,IACE,WAAW,MAAM,UAAU,KAAK,KAAK;AAAA,EACvC,IACA;AAAA,IACE,WAAW,MAAM;AAAA,IAAC;AAAA,EACpB;AAEJ,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,QAAQ,QAAQ;AAAA,IAChB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMZ,OAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,oBAAoB;AAClB,aAAO,OAAO,mBAAmB;AAAA,IACnC;AAAA,IACA,4BAA4B,UAAwC;AAClE,aAAO,UAAU,QAAQ;AAEzB,aAAO,MAAM;AACX,eAAO,YAAY,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,iBAAiB;AACf,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/worker/index.ts"],"sourcesContent":["import {\n AgentSecret,\n CryptoProvider,\n LocalNode,\n Peer,\n StorageAPI,\n} from \"cojson\";\nimport {\n type AnyWebSocketConstructor,\n WebSocketPeerWithReconnection,\n} from \"cojson-transport-ws\";\nimport { WasmCrypto } from \"cojson/crypto/WasmCrypto\";\nimport {\n Account,\n AccountClass,\n AnyAccountSchema,\n CoValueFromRaw,\n Inbox,\n InstanceOfSchema,\n Loaded,\n createJazzContextFromExistingCredentials,\n MockSessionProvider,\n} from \"jazz-tools\";\n\ntype WorkerOptions<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n> = {\n accountID?: string;\n accountSecret?: string;\n syncServer?: string;\n WebSocket?: AnyWebSocketConstructor;\n AccountSchema?: S;\n crypto?: CryptoProvider;\n /**\n * If true, the inbox will not be loaded.\n */\n skipInboxLoad?: boolean;\n /**\n * If false, the worker will not set in the global account context\n */\n asActiveAccount?: boolean;\n storage?: StorageAPI;\n};\n\n/** @category Context Creation */\nexport async function startWorker<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n>(options: WorkerOptions<S>) {\n const {\n accountID = process.env.JAZZ_WORKER_ACCOUNT,\n accountSecret = process.env.JAZZ_WORKER_SECRET,\n syncServer = \"wss://cloud.jazz.tools\",\n AccountSchema = Account as unknown as S,\n skipInboxLoad = false,\n asActiveAccount = true,\n } = options;\n\n let node: LocalNode | undefined = undefined;\n\n const peers: Peer[] = [];\n\n const wsPeer = new WebSocketPeerWithReconnection({\n peer: syncServer,\n reconnectionTimeout: 100,\n addPeer: (peer) => {\n if (node) {\n node.syncManager.addPeer(peer);\n } else {\n peers.push(peer);\n }\n },\n removePeer: () => {},\n WebSocketConstructor: options.WebSocket,\n });\n\n wsPeer.enable();\n\n if (!accountID) {\n throw new Error(\"No accountID provided\");\n }\n if (!accountSecret) {\n throw new Error(\"No accountSecret provided\");\n }\n if (!accountID.startsWith(\"co_\")) {\n throw new Error(\"Invalid accountID\");\n }\n if (!accountSecret?.startsWith(\"sealerSecret_\")) {\n throw new Error(\"Invalid accountSecret\");\n }\n\n const context = await createJazzContextFromExistingCredentials({\n credentials: {\n accountID: accountID,\n secret: accountSecret as AgentSecret,\n },\n AccountSchema,\n sessionProvider: new MockSessionProvider(),\n peers,\n crypto: options.crypto ?? (await WasmCrypto.create()),\n asActiveAccount,\n storage: options.storage,\n });\n\n const account = context.account as InstanceOfSchema<S>;\n node = account.$jazz.localNode;\n\n if (!account.$jazz.refs.profile?.id) {\n throw new Error(\"Account has no profile\");\n }\n\n const inbox = skipInboxLoad ? undefined : await Inbox.load(account);\n\n async function done() {\n await context.account.$jazz.waitForAllCoValuesSync();\n\n wsPeer.disable();\n context.done();\n }\n\n const inboxPublicApi = inbox\n ? {\n subscribe: inbox.subscribe.bind(inbox) as Inbox[\"subscribe\"],\n }\n : {\n subscribe: () => {},\n };\n\n return {\n /**\n * The worker account instance.\n */\n worker: context.account as Loaded<S>,\n experimental: {\n /**\n * API to subscribe to the inbox messages.\n *\n * More info on the Inbox API: https://jazz.tools/docs/react/server-side/inbox\n */\n inbox: inboxPublicApi,\n },\n /**\n * Wait for the connection to the sync server to be established.\n *\n * If already connected, it will resolve immediately.\n */\n waitForConnection() {\n return wsPeer.waitUntilConnected();\n },\n subscribeToConnectionChange(listener: (connected: boolean) => void) {\n wsPeer.subscribe(listener);\n\n return () => {\n wsPeer.unsubscribe(listener);\n };\n },\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n *\n * @deprecated Use shutdownWorker\n */\n done,\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n */\n shutdownWorker() {\n return done();\n },\n };\n}\n"],"mappings":";AAOA;AAAA,EAEE;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EAIA;AAAA,EAGA;AAAA,EACA;AAAA,OACK;AAyBP,eAAsB,YAIpB,SAA2B;AAC3B,QAAM;AAAA,IACJ,YAAY,QAAQ,IAAI;AAAA,IACxB,gBAAgB,QAAQ,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,EACpB,IAAI;AAEJ,MAAI,OAA8B;AAElC,QAAM,QAAgB,CAAC;AAEvB,QAAM,SAAS,IAAI,8BAA8B;AAAA,IAC/C,MAAM;AAAA,IACN,qBAAqB;AAAA,IACrB,SAAS,CAAC,SAAS;AACjB,UAAI,MAAM;AACR,aAAK,YAAY,QAAQ,IAAI;AAAA,MAC/B,OAAO;AACL,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,IACA,YAAY,MAAM;AAAA,IAAC;AAAA,IACnB,sBAAsB,QAAQ;AAAA,EAChC,CAAC;AAED,SAAO,OAAO;AAEd,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AACA,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AACA,MAAI,CAAC,UAAU,WAAW,KAAK,GAAG;AAChC,UAAM,IAAI,MAAM,mBAAmB;AAAA,EACrC;AACA,MAAI,CAAC,eAAe,WAAW,eAAe,GAAG;AAC/C,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAEA,QAAM,UAAU,MAAM,yCAAyC;AAAA,IAC7D,aAAa;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA;AAAA,IACA,iBAAiB,IAAI,oBAAoB;AAAA,IACzC;AAAA,IACA,QAAQ,QAAQ,UAAW,MAAM,WAAW,OAAO;AAAA,IACnD;AAAA,IACA,SAAS,QAAQ;AAAA,EACnB,CAAC;AAED,QAAM,UAAU,QAAQ;AACxB,SAAO,QAAQ,MAAM;AAErB,MAAI,CAAC,QAAQ,MAAM,KAAK,SAAS,IAAI;AACnC,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,QAAM,QAAQ,gBAAgB,SAAY,MAAM,MAAM,KAAK,OAAO;AAElE,iBAAe,OAAO;AACpB,UAAM,QAAQ,QAAQ,MAAM,uBAAuB;AAEnD,WAAO,QAAQ;AACf,YAAQ,KAAK;AAAA,EACf;AAEA,QAAM,iBAAiB,QACnB;AAAA,IACE,WAAW,MAAM,UAAU,KAAK,KAAK;AAAA,EACvC,IACA;AAAA,IACE,WAAW,MAAM;AAAA,IAAC;AAAA,EACpB;AAEJ,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,QAAQ,QAAQ;AAAA,IAChB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMZ,OAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,oBAAoB;AAClB,aAAO,OAAO,mBAAmB;AAAA,IACnC;AAAA,IACA,4BAA4B,UAAwC;AAClE,aAAO,UAAU,QAAQ;AAEzB,aAAO,MAAM;AACX,eAAO,YAAY,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,iBAAiB;AACf,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -205,7 +205,7 @@
205
205
  },
206
206
  "type": "module",
207
207
  "license": "MIT",
208
- "version": "0.19.12",
208
+ "version": "0.19.13",
209
209
  "dependencies": {
210
210
  "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
211
211
  "@scure/base": "1.2.1",
@@ -222,9 +222,9 @@
222
222
  "prosemirror-transform": "^1.9.0",
223
223
  "use-sync-external-store": "^1.5.0",
224
224
  "zod": "4.1.11",
225
- "cojson": "0.19.12",
226
- "cojson-storage-indexeddb": "0.19.12",
227
- "cojson-transport-ws": "0.19.12"
225
+ "cojson": "0.19.13",
226
+ "cojson-storage-indexeddb": "0.19.13",
227
+ "cojson-transport-ws": "0.19.13"
228
228
  },
229
229
  "devDependencies": {
230
230
  "@scure/bip39": "^1.3.0",
@@ -1,4 +1,4 @@
1
- import { LocalNode, Peer, RawAccountID } from "cojson";
1
+ import { LocalNode, Peer } from "cojson";
2
2
  import { getIndexedDBStorage } from "cojson-storage-indexeddb";
3
3
  import { WebSocketPeerWithReconnection } from "cojson-transport-ws";
4
4
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
@@ -23,6 +23,7 @@ import {
23
23
  import { createJazzContext } from "jazz-tools";
24
24
  import { StorageConfig, getStorageOptions } from "./storageOptions.js";
25
25
  import { setupInspector } from "./utils/export-account-inspector.js";
26
+ import { getBrowserLockSessionProvider } from "./provideBrowserLockSession/index.js";
26
27
 
27
28
  setupInspector();
28
29
 
@@ -210,7 +211,7 @@ export async function createJazzBrowserContext<
210
211
  crypto,
211
212
  defaultProfileName: options.defaultProfileName,
212
213
  AccountSchema: options.AccountSchema,
213
- sessionProvider: provideBrowserLockSession,
214
+ sessionProvider: getBrowserLockSessionProvider(),
214
215
  authSecretStorage: options.authSecretStorage,
215
216
  });
216
217
 
@@ -240,66 +241,6 @@ export type SessionProvider = (
240
241
  accountID: ID<Account> | AgentID,
241
242
  ) => Promise<SessionID>;
242
243
 
243
- export function provideBrowserLockSession(
244
- accountID: ID<Account> | AgentID,
245
- crypto: CryptoProvider,
246
- ) {
247
- if (typeof navigator === "undefined" || !navigator.locks?.request) {
248
- // Fallback to random session ID for each tab session
249
- return Promise.resolve({
250
- sessionID: crypto.newRandomSessionID(accountID as RawAccountID | AgentID),
251
- sessionDone: () => {},
252
- });
253
- }
254
-
255
- let sessionDone!: () => void;
256
- const donePromise = new Promise<void>((resolve) => {
257
- sessionDone = resolve;
258
- });
259
-
260
- let resolveSession: (sessionID: SessionID) => void;
261
- const sessionPromise = new Promise<SessionID>((resolve) => {
262
- resolveSession = resolve;
263
- });
264
-
265
- void (async function () {
266
- for (let idx = 0; idx < 100; idx++) {
267
- // To work better around StrictMode
268
- for (let retry = 0; retry < 2; retry++) {
269
- // console.debug("Trying to get lock", accountID + "_" + idx);
270
- const sessionFinishedOrNoLock = await navigator.locks.request(
271
- accountID + "_" + idx,
272
- { ifAvailable: true },
273
- async (lock) => {
274
- if (!lock) return "noLock";
275
-
276
- const sessionID =
277
- localStorage.getItem(accountID + "_" + idx) ||
278
- crypto.newRandomSessionID(accountID as RawAccountID | AgentID);
279
- localStorage.setItem(accountID + "_" + idx, sessionID);
280
-
281
- resolveSession(sessionID as SessionID);
282
-
283
- await donePromise;
284
- console.log("Done with lock", accountID + "_" + idx, sessionID);
285
- return "sessionFinished";
286
- },
287
- );
288
-
289
- if (sessionFinishedOrNoLock === "sessionFinished") {
290
- return;
291
- }
292
- }
293
- }
294
- throw new Error("Couldn't get lock on session after 100x2 tries");
295
- })();
296
-
297
- return sessionPromise.then((sessionID) => ({
298
- sessionID,
299
- sessionDone,
300
- }));
301
- }
302
-
303
244
  /** @category Invite Links */
304
245
  export function createInviteLink<C extends CoValue>(
305
246
  value: C,
@@ -0,0 +1,406 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { WasmCrypto } from "cojson/crypto/WasmCrypto";
4
+ import { SessionID } from "cojson";
5
+ import { beforeEach, describe, expect, test, vi } from "vitest";
6
+ import { BrowserSessionProvider } from "./BrowserSessionProvider.js";
7
+ import { SessionIDStorage } from "./SessionIDStorage.js";
8
+ import { createJazzTestAccount } from "jazz-tools/testing";
9
+ import type { CryptoProvider } from "jazz-tools";
10
+
11
+ const Crypto = await WasmCrypto.create();
12
+
13
+ // Mock navigator.locks
14
+ interface LockInfo {
15
+ mode: "exclusive" | "shared";
16
+ release: () => void;
17
+ }
18
+
19
+ const mockLocks = new Map<string, LockInfo>();
20
+
21
+ function isLockAvailable(
22
+ name: string,
23
+ requestedMode: "exclusive" | "shared",
24
+ ): boolean {
25
+ const existingLock = mockLocks.get(name);
26
+ if (!existingLock) {
27
+ return true; // No existing lock
28
+ }
29
+
30
+ // Exclusive locks block everything
31
+ if (existingLock.mode === "exclusive" || requestedMode === "exclusive") {
32
+ return false;
33
+ }
34
+
35
+ // Shared locks can coexist with other shared locks
36
+ if (existingLock.mode === "shared" && requestedMode === "shared") {
37
+ return true;
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ function createMockLock(
44
+ name: string,
45
+ mode: "exclusive" | "shared",
46
+ ): {
47
+ lock: { name: string } | null;
48
+ release: () => void;
49
+ } {
50
+ if (!isLockAvailable(name, mode)) {
51
+ return { lock: null, release: () => {} };
52
+ }
53
+
54
+ // Create new lock
55
+ const lockInfo: LockInfo = {
56
+ mode,
57
+ release: () => {
58
+ mockLocks.delete(name);
59
+ },
60
+ };
61
+
62
+ mockLocks.set(name, lockInfo);
63
+
64
+ return {
65
+ lock: { name },
66
+ release: lockInfo.release,
67
+ };
68
+ }
69
+
70
+ vi.stubGlobal("navigator", {
71
+ locks: {
72
+ request: vi.fn(
73
+ async (
74
+ name: string,
75
+ options: { mode?: "exclusive" | "shared"; ifAvailable?: boolean },
76
+ callback: (lock: { name: string } | null) => Promise<void> | void,
77
+ ) => {
78
+ const mode = options?.mode || "exclusive";
79
+ const ifAvailable = options?.ifAvailable || false;
80
+
81
+ if (ifAvailable) {
82
+ const { lock } = createMockLock(name, mode);
83
+ if (!lock) {
84
+ // Lock not available, call callback with null and return immediately
85
+ await callback(null);
86
+ return;
87
+ }
88
+ // Lock available, call callback with lock
89
+ // The lock is held until the promise returned from callback resolves
90
+ const callbackPromise = callback(lock);
91
+ const result = await callbackPromise;
92
+ // Release lock after callback promise resolves
93
+ mockLocks.get(name)?.release();
94
+ return result;
95
+ }
96
+
97
+ // For non-ifAvailable locks, wait until available
98
+ // In a real implementation, this would wait, but for tests we assume immediate availability
99
+ const { lock, release } = createMockLock(name, mode);
100
+ if (!lock) {
101
+ // This shouldn't happen in tests for non-ifAvailable locks
102
+ // But handle it gracefully
103
+ await callback(null);
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const callbackPromise = callback(lock);
109
+ const result = await callbackPromise;
110
+ return result;
111
+ } finally {
112
+ release();
113
+ }
114
+ },
115
+ ),
116
+ },
117
+ });
118
+
119
+ describe("BrowserSessionProvider", () => {
120
+ let sessionProvider: BrowserSessionProvider;
121
+ let account: Awaited<ReturnType<typeof createJazzTestAccount>>;
122
+
123
+ beforeEach(async () => {
124
+ // Clear localStorage
125
+ localStorage.clear();
126
+
127
+ // Clear mock locks
128
+ mockLocks.clear();
129
+
130
+ // Create new session provider instance
131
+ sessionProvider = new BrowserSessionProvider();
132
+
133
+ // Create test account
134
+ account = await createJazzTestAccount({
135
+ isCurrentActiveAccount: true,
136
+ });
137
+ });
138
+
139
+ describe("acquireSession", () => {
140
+ test("creates new session when none exists", async () => {
141
+ const accountID = account.$jazz.id;
142
+
143
+ // Verify no sessions exist
144
+ const existingSessionsBefore =
145
+ SessionIDStorage.getSessionsList(accountID);
146
+ expect(existingSessionsBefore).toEqual([]);
147
+
148
+ // Acquire session
149
+ const result = await sessionProvider.acquireSession(
150
+ accountID,
151
+ Crypto as CryptoProvider,
152
+ );
153
+
154
+ // Verify a new session ID is generated
155
+ expect(result.sessionID).toBeDefined();
156
+
157
+ // Verify the session is stored in localStorage
158
+ const storedSessions = SessionIDStorage.getSessionsList(accountID);
159
+ expect(storedSessions).toHaveLength(1);
160
+ expect(storedSessions[0]).toBe(result.sessionID);
161
+ });
162
+
163
+ test("returns existing session when available", async () => {
164
+ const accountID = account.$jazz.id;
165
+ const existingSessionID = "existing-session-id" as SessionID;
166
+
167
+ // Pre-populate localStorage with a session ID
168
+ SessionIDStorage.storeSessionID(accountID, existingSessionID, 0);
169
+
170
+ // Verify session exists before calling acquireSession
171
+ const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
172
+ expect(sessionsBefore).toHaveLength(1);
173
+ expect(sessionsBefore[0]).toBe(existingSessionID);
174
+
175
+ // Acquire session
176
+ const result = await sessionProvider.acquireSession(
177
+ accountID,
178
+ Crypto as CryptoProvider,
179
+ );
180
+
181
+ // Verify the existing session ID is returned (not a new one)
182
+ expect(result.sessionID).toBe(existingSessionID);
183
+
184
+ // Verify no new session is created (same value still in store)
185
+ const sessionsAfter = SessionIDStorage.getSessionsList(accountID);
186
+ expect(sessionsAfter).toHaveLength(1);
187
+ expect(sessionsAfter[0]).toBe(existingSessionID);
188
+ expect(sessionsAfter[0]).toBe(result.sessionID);
189
+ });
190
+
191
+ test("handles multiple sessions in list - skips locked sessions and returns next available", async () => {
192
+ const accountID = account.$jazz.id;
193
+ const session1 = "session-1" as SessionID;
194
+ const session2 = "session-2" as SessionID;
195
+ const session3 = "session-3" as SessionID;
196
+
197
+ // Pre-populate localStorage with multiple sessions
198
+ SessionIDStorage.storeSessionID(accountID, session1, 0);
199
+ SessionIDStorage.storeSessionID(accountID, session2, 1);
200
+ SessionIDStorage.storeSessionID(accountID, session3, 2);
201
+
202
+ // Verify sessions are stored
203
+ const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
204
+ expect(sessionsBefore).toHaveLength(3);
205
+
206
+ // Lock the first session (index 0) by manually adding it to mockLocks
207
+ const lockName = `load_session_${session1}`;
208
+ mockLocks.set(lockName, {
209
+ mode: "exclusive",
210
+ release: () => {
211
+ mockLocks.delete(lockName);
212
+ },
213
+ });
214
+
215
+ // Acquire session - should skip locked session1 and return session2
216
+ const result = await sessionProvider.acquireSession(
217
+ accountID,
218
+ Crypto as CryptoProvider,
219
+ );
220
+
221
+ // Verify it returned session2 (next available, not session1 which is locked)
222
+ expect(result.sessionID).toBe(session2);
223
+
224
+ // Verify the returned session is from the existing list, not a new one
225
+ const allSessions = SessionIDStorage.getSessionsList(accountID);
226
+ expect(allSessions).toContain(result.sessionID);
227
+ expect([session1, session2, session3]).toContain(result.sessionID);
228
+
229
+ // Clean up the held lock
230
+ mockLocks.delete(lockName);
231
+ });
232
+
233
+ test("creates new session when all existing sessions are locked", async () => {
234
+ const accountID = account.$jazz.id;
235
+ const session1 = "session-1" as SessionID;
236
+ const session2 = "session-2" as SessionID;
237
+
238
+ // Pre-populate localStorage with sessions
239
+ SessionIDStorage.storeSessionID(accountID, session1, 0);
240
+ SessionIDStorage.storeSessionID(accountID, session2, 1);
241
+
242
+ // Verify sessions are stored
243
+ const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
244
+ expect(sessionsBefore).toHaveLength(2);
245
+
246
+ // Lock all existing sessions by manually adding them to mockLocks
247
+ const lock1Name = `load_session_${session1}`;
248
+ const lock2Name = `load_session_${session2}`;
249
+ mockLocks.set(lock1Name, {
250
+ mode: "exclusive",
251
+ release: () => {
252
+ mockLocks.delete(lock1Name);
253
+ },
254
+ });
255
+ mockLocks.set(lock2Name, {
256
+ mode: "exclusive",
257
+ release: () => {
258
+ mockLocks.delete(lock2Name);
259
+ },
260
+ });
261
+
262
+ // Acquire session - should create a new one since all existing are locked
263
+ const result = await sessionProvider.acquireSession(
264
+ accountID,
265
+ Crypto as CryptoProvider,
266
+ );
267
+
268
+ // Verify a new session is created (not one of the existing locked ones)
269
+ expect(result.sessionID).not.toBe(session1);
270
+ expect(result.sessionID).not.toBe(session2);
271
+
272
+ // Verify the new session is added to localStorage
273
+ const sessionsAfter = SessionIDStorage.getSessionsList(accountID);
274
+ expect(sessionsAfter).toHaveLength(3);
275
+ expect(sessionsAfter).toContain(result.sessionID);
276
+
277
+ // Clean up held locks
278
+ mockLocks.delete(lock1Name);
279
+ mockLocks.delete(lock2Name);
280
+ });
281
+
282
+ test("releases lock when sessionDone is called", async () => {
283
+ const accountID = account.$jazz.id;
284
+
285
+ // Acquire a session
286
+ const result = await sessionProvider.acquireSession(
287
+ accountID,
288
+ Crypto as CryptoProvider,
289
+ );
290
+
291
+ const sessionID = result.sessionID;
292
+
293
+ // Call sessionDone to release the lock
294
+ result.sessionDone();
295
+
296
+ // Wait for async lock release
297
+ await new Promise((resolve) => setTimeout(resolve, 0));
298
+
299
+ // Acquire a session
300
+ const result2 = await sessionProvider.acquireSession(
301
+ accountID,
302
+ Crypto as CryptoProvider,
303
+ );
304
+
305
+ expect(result2.sessionID).toBe(sessionID);
306
+ });
307
+ });
308
+
309
+ describe("persistSession", () => {
310
+ test("stores session ID correctly", async () => {
311
+ const accountID = account.$jazz.id;
312
+ const sessionID = "test-session-id" as SessionID;
313
+
314
+ // Verify no sessions exist before
315
+ const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
316
+ expect(sessionsBefore).toEqual([]);
317
+
318
+ // Persist session
319
+ const result = await sessionProvider.persistSession(accountID, sessionID);
320
+
321
+ // Verify the session ID is stored in localStorage at index 0
322
+ const storedSessions = SessionIDStorage.getSessionsList(accountID);
323
+ expect(storedSessions).toHaveLength(1);
324
+ expect(storedSessions[0]).toBe(sessionID);
325
+
326
+ // Verify sessionDone callback is provided
327
+ expect(typeof result.sessionDone).toBe("function");
328
+ });
329
+
330
+ test("adds to sessions list properly", async () => {
331
+ const accountID = account.$jazz.id;
332
+ const initialSessionID = "initial-session-id" as SessionID;
333
+ const newSessionID = "new-session-id" as SessionID;
334
+
335
+ // Pre-populate localStorage with one session (index 0)
336
+ SessionIDStorage.storeSessionID(accountID, initialSessionID, 0);
337
+
338
+ // Verify initial session is stored
339
+ const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
340
+ expect(sessionsBefore).toHaveLength(1);
341
+ expect(sessionsBefore[0]).toBe(initialSessionID);
342
+
343
+ // Persist a new session ID
344
+ await sessionProvider.persistSession(accountID, newSessionID);
345
+
346
+ // Verify the new session is stored at index 1
347
+ const sessionsAfter = SessionIDStorage.getSessionsList(accountID);
348
+ expect(sessionsAfter).toHaveLength(2);
349
+ expect(sessionsAfter[0]).toBe(initialSessionID);
350
+ expect(sessionsAfter[1]).toBe(newSessionID);
351
+ });
352
+
353
+ test("locks session when persisting", async () => {
354
+ const accountID = account.$jazz.id;
355
+ const sessionID = "persisted-session-id" as SessionID;
356
+
357
+ // Persist session - this should acquire a lock on the session
358
+ const { sessionDone } = await sessionProvider.persistSession(
359
+ accountID,
360
+ sessionID,
361
+ );
362
+
363
+ // Verify the session is locked by checking mockLocks directly
364
+ // The lock should be held until sessionDone is called
365
+ const lockName = `load_session_${sessionID}`;
366
+ expect(mockLocks.has(lockName)).toBe(true);
367
+
368
+ // Also verify we can't acquire the lock while it's held
369
+ // (This tests the isLockAvailable function)
370
+ expect(isLockAvailable(lockName, "exclusive")).toBe(false);
371
+
372
+ // Clean up by releasing the lock
373
+ sessionDone();
374
+
375
+ // After releasing, the lock should be removed
376
+ // Note: The lock might be removed asynchronously, so we check after a small delay
377
+ await new Promise((resolve) => setTimeout(resolve, 0));
378
+ expect(mockLocks.has(lockName)).toBe(false);
379
+ });
380
+
381
+ test("releases lock when sessionDone is called", async () => {
382
+ const accountID = account.$jazz.id;
383
+ const sessionID = "session-to-release" as SessionID;
384
+
385
+ // Persist a session
386
+ const { sessionDone } = await sessionProvider.persistSession(
387
+ accountID,
388
+ sessionID,
389
+ );
390
+
391
+ // Call sessionDone to release the lock
392
+ sessionDone();
393
+
394
+ // Wait for async lock release
395
+ await new Promise((resolve) => setTimeout(resolve, 0));
396
+
397
+ // Acquire a session
398
+ const result = await sessionProvider.acquireSession(
399
+ accountID,
400
+ Crypto as CryptoProvider,
401
+ );
402
+
403
+ expect(result.sessionID).toBe(sessionID);
404
+ });
405
+ });
406
+ });