@dittolive/ditto 4.5.1-experimental.aarch64-linux.1.aarch64 → 4.5.2-rc.2

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 (152) hide show
  1. package/DittoReactNative.podspec +25 -0
  2. package/README.md +2 -2
  3. package/node/ditto.cjs.js +1 -1
  4. package/node/ditto.cjs.js.map +1 -0
  5. package/node/ditto.cjs.pretty.js +9655 -0
  6. package/node/ditto.cjs.pretty.js.map +1 -0
  7. package/node/ditto.darwin-arm64.node +0 -0
  8. package/node/ditto.darwin-x64.node +0 -0
  9. package/node/{ditto.linux-arm64.node → ditto.linux-x64.node} +0 -0
  10. package/node/transports.darwin-arm64.node +0 -0
  11. package/node/transports.darwin-x64.node +0 -0
  12. package/package.json +2 -1
  13. package/react-native/android/CMakeLists.txt +37 -0
  14. package/react-native/android/build.gradle +186 -0
  15. package/react-native/android/cpp-adapter.cpp +254 -0
  16. package/react-native/android/gradle.properties +5 -0
  17. package/react-native/android/src/main/AndroidManifest.xml +4 -0
  18. package/react-native/android/src/main/java/com/dittolive/rnsdk/DittoRNSDKModule.java +85 -0
  19. package/react-native/android/src/main/java/com/dittolive/rnsdk/DittoRNSDKPackage.java +28 -0
  20. package/react-native/cpp/include/Arc.hpp +141 -0
  21. package/react-native/cpp/include/Attachment.h +16 -0
  22. package/react-native/cpp/include/Authentication.h +23 -0
  23. package/react-native/cpp/include/Collection.h +13 -0
  24. package/react-native/cpp/include/DQL.h +21 -0
  25. package/react-native/cpp/include/Document.h +17 -0
  26. package/react-native/cpp/include/Identity.h +17 -0
  27. package/react-native/cpp/include/Lifecycle.h +17 -0
  28. package/react-native/cpp/include/LiveQuery.h +17 -0
  29. package/react-native/cpp/include/Logger.h +22 -0
  30. package/react-native/cpp/include/Misc.h +27 -0
  31. package/react-native/cpp/include/Presence.h +14 -0
  32. package/react-native/cpp/include/SmallPeerInfo.h +19 -0
  33. package/react-native/cpp/include/Transports.h +25 -0
  34. package/react-native/cpp/include/TypedArray.hpp +167 -0
  35. package/react-native/cpp/include/Utils.h +61 -0
  36. package/react-native/cpp/include/main.h +10 -0
  37. package/react-native/cpp/src/Attachment.cpp +86 -0
  38. package/react-native/cpp/src/Authentication.cpp +227 -0
  39. package/react-native/cpp/src/Collection.cpp +54 -0
  40. package/react-native/cpp/src/DQL.cpp +256 -0
  41. package/react-native/cpp/src/Document.cpp +146 -0
  42. package/react-native/cpp/src/Identity.cpp +123 -0
  43. package/react-native/cpp/src/Lifecycle.cpp +110 -0
  44. package/react-native/cpp/src/LiveQuery.cpp +63 -0
  45. package/react-native/cpp/src/Logger.cpp +200 -0
  46. package/react-native/cpp/src/Misc.cpp +283 -0
  47. package/react-native/cpp/src/Presence.cpp +79 -0
  48. package/react-native/cpp/src/SmallPeerInfo.cpp +142 -0
  49. package/react-native/cpp/src/Transports.cpp +270 -0
  50. package/react-native/cpp/src/TypedArray.cpp +303 -0
  51. package/react-native/cpp/src/Utils.cpp +138 -0
  52. package/react-native/cpp/src/main.cpp +152 -0
  53. package/react-native/dittoffi/dittoffi.h +4700 -0
  54. package/react-native/dittoffi/ifaddrs.cpp +385 -0
  55. package/react-native/dittoffi/ifaddrs.h +206 -0
  56. package/react-native/ios/DittoRNSDK.h +7 -0
  57. package/react-native/ios/DittoRNSDK.mm +107 -0
  58. package/react-native/ios/YeetJSIUtils.h +60 -0
  59. package/react-native/ios/YeetJSIUtils.mm +196 -0
  60. package/react-native/lib/commonjs/ditto.rn.js +92 -0
  61. package/react-native/lib/commonjs/ditto.rn.js.map +1 -0
  62. package/react-native/lib/commonjs/index.js +61 -0
  63. package/react-native/lib/commonjs/index.js.map +1 -0
  64. package/react-native/lib/module/ditto.rn.js +88 -0
  65. package/react-native/lib/module/ditto.rn.js.map +1 -0
  66. package/react-native/lib/module/index.js +27 -0
  67. package/react-native/lib/module/index.js.map +1 -0
  68. package/react-native/lib/typescript/ditto.rn.d.ts +15 -0
  69. package/react-native/lib/typescript/ditto.rn.d.ts.map +1 -0
  70. package/react-native/lib/typescript/index.d.ts +1 -0
  71. package/react-native/lib/typescript/index.d.ts.map +1 -0
  72. package/react-native/src/ditto.rn.ts +91 -0
  73. package/react-native/src/environment/environment.fallback.ts +4 -0
  74. package/react-native/src/index.ts +26 -0
  75. package/react-native/src/sources/@cbor-redux.ts +2 -0
  76. package/react-native/src/sources/@ditto.core.ts +1 -0
  77. package/react-native/src/sources/@environment.ts +1 -0
  78. package/react-native/src/sources/attachment-fetch-event.ts +54 -0
  79. package/react-native/src/sources/attachment-fetcher-manager.ts +144 -0
  80. package/react-native/src/sources/attachment-fetcher.ts +134 -0
  81. package/react-native/src/sources/attachment-token.ts +48 -0
  82. package/react-native/src/sources/attachment.ts +74 -0
  83. package/react-native/src/sources/augment.ts +101 -0
  84. package/react-native/src/sources/authenticator.ts +314 -0
  85. package/react-native/src/sources/base-pending-cursor-operation.ts +239 -0
  86. package/react-native/src/sources/base-pending-id-specific-operation.ts +109 -0
  87. package/react-native/src/sources/bridge.ts +553 -0
  88. package/react-native/src/sources/build-time-constants.ts +8 -0
  89. package/react-native/src/sources/cbor.ts +35 -0
  90. package/react-native/src/sources/collection-interface.ts +67 -0
  91. package/react-native/src/sources/collection.ts +212 -0
  92. package/react-native/src/sources/collections-event.ts +99 -0
  93. package/react-native/src/sources/counter.ts +82 -0
  94. package/react-native/src/sources/ditto.ts +979 -0
  95. package/react-native/src/sources/document-id.ts +159 -0
  96. package/react-native/src/sources/document-path.ts +306 -0
  97. package/react-native/src/sources/document.ts +193 -0
  98. package/react-native/src/sources/epilogue.ts +30 -0
  99. package/react-native/src/sources/error-codes.ts +52 -0
  100. package/react-native/src/sources/error.ts +208 -0
  101. package/react-native/src/sources/essentials.ts +53 -0
  102. package/react-native/src/sources/ffi-error.ts +122 -0
  103. package/react-native/src/sources/ffi.ts +2012 -0
  104. package/react-native/src/sources/identity.ts +163 -0
  105. package/react-native/src/sources/init.ts +71 -0
  106. package/react-native/src/sources/internal.ts +109 -0
  107. package/react-native/src/sources/keep-alive.ts +73 -0
  108. package/react-native/src/sources/key-path.ts +198 -0
  109. package/react-native/src/sources/live-query-event.ts +208 -0
  110. package/react-native/src/sources/live-query-manager.ts +102 -0
  111. package/react-native/src/sources/live-query.ts +166 -0
  112. package/react-native/src/sources/logger.ts +196 -0
  113. package/react-native/src/sources/main.ts +60 -0
  114. package/react-native/src/sources/observer-manager.ts +178 -0
  115. package/react-native/src/sources/observer.ts +79 -0
  116. package/react-native/src/sources/pending-collections-operation.ts +232 -0
  117. package/react-native/src/sources/pending-cursor-operation.ts +218 -0
  118. package/react-native/src/sources/pending-id-specific-operation.ts +218 -0
  119. package/react-native/src/sources/presence-manager.ts +161 -0
  120. package/react-native/src/sources/presence.ts +233 -0
  121. package/react-native/src/sources/query-result-item.ts +116 -0
  122. package/react-native/src/sources/query-result.ts +55 -0
  123. package/react-native/src/sources/register.ts +95 -0
  124. package/react-native/src/sources/small-peer-info.ts +177 -0
  125. package/react-native/src/sources/static-tcp-client.ts +6 -0
  126. package/react-native/src/sources/store-observer.ts +177 -0
  127. package/react-native/src/sources/store.ts +385 -0
  128. package/react-native/src/sources/subscription-manager.ts +99 -0
  129. package/react-native/src/sources/subscription.ts +89 -0
  130. package/react-native/src/sources/sync-subscription.ts +90 -0
  131. package/react-native/src/sources/sync.ts +559 -0
  132. package/react-native/src/sources/test-helpers.ts +24 -0
  133. package/react-native/src/sources/transport-conditions-manager.ts +104 -0
  134. package/react-native/src/sources/transport-config.ts +430 -0
  135. package/react-native/src/sources/update-result.ts +66 -0
  136. package/react-native/src/sources/update-results-map.ts +57 -0
  137. package/react-native/src/sources/websocket-client.ts +7 -0
  138. package/react-native/src/sources/write-transaction-collection.ts +122 -0
  139. package/react-native/src/sources/write-transaction-pending-cursor-operation.ts +101 -0
  140. package/react-native/src/sources/write-transaction-pending-id-specific-operation.ts +74 -0
  141. package/react-native/src/sources/write-transaction.ts +121 -0
  142. package/react-native.config.js +9 -0
  143. package/types/ditto.d.ts.map +1 -0
  144. package/web/ditto.es6.js +1 -1
  145. package/web/ditto.es6.js.map +1 -0
  146. package/web/ditto.es6.pretty.js +12600 -0
  147. package/web/ditto.es6.pretty.js.map +1 -0
  148. package/web/ditto.umd.js +1 -1
  149. package/web/ditto.umd.js.map +1 -0
  150. package/web/ditto.umd.pretty.js +12669 -0
  151. package/web/ditto.umd.pretty.js.map +1 -0
  152. package/web/ditto.wasm +0 -0
@@ -0,0 +1,177 @@
1
+ //
2
+ // Copyright © 2023 DittoLive Incorporated. All rights reserved.
3
+ //
4
+
5
+ import * as FFI from './ffi'
6
+
7
+ import { Bridge } from './bridge'
8
+ import { CBOR, documentIDReplacer } from './cbor'
9
+ import { DittoError, mapFFIErrors } from './error'
10
+ import { Logger } from './logger'
11
+ import { QueryResult } from './query-result'
12
+
13
+ import type { Ditto } from './ditto'
14
+ import type { DQLQueryArguments } from './essentials'
15
+ import type { Store } from './store'
16
+
17
+ /**
18
+ * A store observation handler is called whenever an active store observer
19
+ * receives new results.
20
+ */
21
+ export type StoreObservationHandler = (queryResult: QueryResult) => void
22
+
23
+ /**
24
+ * A store observation handler is called whenever an active store observer
25
+ * receives new results.
26
+ *
27
+ * Call `signalNext()` to signal that the handler is ready to receive the next
28
+ * callback from the store observer.
29
+ */
30
+ export type StoreObservationHandlerWithSignalNext = (queryResult: QueryResult, signalNext: () => void) => void
31
+
32
+ /**
33
+ * A store observer invokes a given handler whenever results for its query
34
+ * change.
35
+ *
36
+ * The store observer will remain active until it is {@link cancel | cancelled},
37
+ * or the Ditto instance managing the observer has been
38
+ * {@link Ditto.close | closed}.
39
+ *
40
+ * Create a store observer by calling
41
+ * {@link Store.registerObserver | `ditto.store.registerObserver()`}.
42
+ */
43
+ export class StoreObserver {
44
+ /**
45
+ * The Ditto instance this store observer is registered with.
46
+ */
47
+ readonly ditto: Ditto
48
+
49
+ /**
50
+ * The query string of the store observer (as passed when registering it).
51
+ */
52
+ readonly queryString: string
53
+
54
+ /**
55
+ * The query arguments of the store observer (as passed when registering it).
56
+ */
57
+ readonly queryArguments?: Readonly<DQLQueryArguments>
58
+
59
+ /**
60
+ * Convenience property, returns `true` once the store observer has been
61
+ * cancelled.
62
+ */
63
+ get isCancelled(): boolean {
64
+ return this._isCancelled
65
+ }
66
+
67
+ /**
68
+ * Cancels the store observer and unregisters it. No-op if the
69
+ * store observer has already been cancelled.
70
+ */
71
+ cancel() {
72
+ if (this._isCancelled) return
73
+ this._isCancelled = true
74
+ this.ditto.store.unregisterObserver(this)
75
+ }
76
+
77
+ // --------------------------- Internal -------------------------------------
78
+
79
+ /**
80
+ * The ID of this observer's live query.
81
+ *
82
+ * @internal
83
+ */
84
+ readonly liveQueryID: number
85
+
86
+ /** @internal */
87
+ constructor(ditto: Ditto, query: string, queryArguments: DQLQueryArguments | null, observationHandler: StoreObservationHandlerWithSignalNext) {
88
+ this.queryString = query
89
+ this.queryArguments = queryArguments ? Object.freeze({ ...queryArguments }) : undefined
90
+ this.ditto = ditto
91
+
92
+ let queryArgumentsCBOR: Uint8Array | null = null
93
+ if (queryArguments != null) {
94
+ try {
95
+ queryArgumentsCBOR = CBOR.encode(queryArguments, documentIDReplacer)
96
+ } catch (error: any) {
97
+ throw new DittoError('query/arguments-invalid')
98
+ }
99
+ }
100
+ let storeObserverID: number | undefined
101
+ const dittoHandle = Bridge.ditto.handleFor(ditto)
102
+ this.ditto.deferClose(() => {
103
+ const weakThis = new WeakRef(this)
104
+
105
+ function wrappedObservationHandler(cCBParams: CCallbackParams): void {
106
+ const strongThis = weakThis.deref()
107
+ if (strongThis == null) {
108
+ Logger.debug(`Ignoring change event received by store observer ${storeObserverID} after it was cancelled`)
109
+ return
110
+ }
111
+
112
+ const response = Bridge.dqlResponse.bridge(cCBParams.query_result, () => new QueryResult(cCBParams.query_result))
113
+
114
+ Logger.debug(`Invoking user event handler with new event for store observer ${storeObserverID}`)
115
+ observationHandler(response, () => {
116
+ strongThis.signalNext()
117
+ })
118
+ }
119
+
120
+ const errorContext = {
121
+ query,
122
+ queryArguments,
123
+ }
124
+
125
+ mapFFIErrors(
126
+ () => {
127
+ storeObserverID = FFI.tryExperimentalRegisterChangeObserver(dittoHandle.deref(), query, queryArgumentsCBOR, wrappedObservationHandler)
128
+ },
129
+ undefined,
130
+ errorContext,
131
+ )
132
+ })
133
+
134
+ if (storeObserverID == null) {
135
+ throw new DittoError('internal', 'Internal inconsistency, store observer ID is undefined after registering')
136
+ }
137
+
138
+ this.liveQueryID = storeObserverID
139
+ }
140
+
141
+ // --------------------------- Private --------------------------------------
142
+
143
+ /**
144
+ * `true` when the store observer has been cancelled.
145
+ *
146
+ * We mark the store observer as cancelled here as an optimization to avoid a
147
+ * scan of all store observers in the store whenever the `isCancelled`
148
+ * property is checked.
149
+ */
150
+ private _isCancelled = false
151
+
152
+ /**
153
+ * Signals to Ditto Core that the observer is ready for the next event.
154
+ */
155
+ private signalNext() {
156
+ const ditto = this.ditto
157
+ if (!ditto || ditto.isClosed) return
158
+
159
+ const dittoHandle = Bridge.ditto.handleFor(ditto)
160
+ const dittoPointer = dittoHandle.derefOrNull()
161
+ if (!dittoPointer) return
162
+
163
+ if (this.liveQueryID == null) {
164
+ throw new Error('live query ID is null while signaling ready for next event')
165
+ }
166
+
167
+ return ditto.deferCloseAsync(async () => {
168
+ Logger.debug(`Signaling availability for live query ${this.liveQueryID}`)
169
+ await FFI.liveQuerySignalAvailableNext(dittoPointer, this.liveQueryID)
170
+ })
171
+ }
172
+ }
173
+
174
+ // c.f. struct c_cb_params
175
+ type CCallbackParams = {
176
+ query_result: FFI.Pointer<FFI.FFIDqlResponse>
177
+ }
@@ -0,0 +1,385 @@
1
+ //
2
+ // Copyright © 2021 DittoLive Incorporated. All rights reserved.
3
+ //
4
+
5
+ import * as FFI from './ffi'
6
+ import { Bridge } from './bridge'
7
+
8
+ import { CBOR, documentIDReplacer } from './cbor'
9
+ import { Collection } from './collection'
10
+ import { StoreObserver } from './store-observer'
11
+ import { DocumentID } from './document-id'
12
+ import { DittoError, mapFFIErrors, mapFFIErrorsAsync } from './error'
13
+ import { performAsyncToWorkaroundNonAsyncFFIAPI, step, validateQuery } from './internal'
14
+ import { Logger } from './logger'
15
+ import { PendingCollectionsOperation } from './pending-collections-operation'
16
+ import { QueryResult } from './query-result'
17
+ import { WriteTransaction } from './write-transaction'
18
+
19
+ import type { StoreObservationHandlerWithSignalNext, StoreObservationHandler } from './store-observer'
20
+ import type { Ditto } from './ditto'
21
+ import type { DQLQueryArguments } from './essentials'
22
+ import type { QueryResultItem } from './query-result-item'
23
+ import type { WriteTransactionResult } from './write-transaction'
24
+
25
+ /**
26
+ * The entrypoint for all actions that relate to data stored by Ditto. Provides
27
+ * access to collections, a write transaction API, and a query hash API.
28
+ *
29
+ * You don't create one directly but can access it from a particular
30
+ * {@link Ditto} instance via its {@link Ditto.store | store} property.
31
+ */
32
+ export class Store {
33
+ /** The {@link Ditto} instance this store belongs to. */
34
+ readonly ditto: Ditto
35
+
36
+ /**
37
+ * All currently active store observers.
38
+ *
39
+ * **Note:** Manage store observers using
40
+ * {@link registerObserver | registerObserver()} to register a new store
41
+ * observer and {@link StoreObserver.cancel | StoreObserver.cancel()} to
42
+ * remove an existing store observer.
43
+ */
44
+ readonly observers: Readonly<Array<StoreObserver>> = Object.freeze([])
45
+
46
+ /**
47
+ * Register a handler to be called whenever a query's results change in the
48
+ * local store.
49
+ *
50
+ * Convenience method, same as
51
+ * {@link registerObserverWithSignalNext | registerObserverWithSignalNext()},
52
+ * except that here, the next invocation of the observation handler is
53
+ * triggered automatically instead of having to call the passed in
54
+ * `signalNext` function.
55
+ *
56
+ * @param query a string containing a valid query expressed in DQL.
57
+ * @param observationHandler a function that is called whenever the query's results
58
+ * change. The function is passed a {@link QueryResult} containing a
59
+ * {@link QueryResultItem} for each match.
60
+ * @param queryArguments an object of values keyed by the placeholder name
61
+ * without the leading `:`. Example: `{ "name": "Joanna" }` for a query like
62
+ * `SELECT * FROM people WHERE name = :name`.
63
+ * @returns a {@link StoreObserver} that can be used to cancel the
64
+ * observation.
65
+ * @throws {@link DittoError} `query/invalid`: if `query` argument is not a
66
+ * string or not valid DQL.
67
+ * @throws {@link DittoError} `query/arguments-invalid`: if `queryArguments`
68
+ * argument is invalid (e.g. contains unsupported types).
69
+ * @throws {@link DittoError} `query/unsupported`: if the query is not a
70
+ * `SELECT` query.
71
+ * @throws {@link DittoError} may throw other errors.
72
+ */
73
+ registerObserver(query: string, observationHandler: StoreObservationHandler, queryArguments?: DQLQueryArguments): StoreObserver {
74
+ const changeHandlerWithSignalNext: StoreObservationHandlerWithSignalNext = (queryResult: QueryResult, signalNext: () => void) => {
75
+ try {
76
+ observationHandler(queryResult)
77
+ } finally {
78
+ signalNext()
79
+ }
80
+ }
81
+ return this.registerObserverWithSignalNext(query, changeHandlerWithSignalNext, queryArguments)
82
+ }
83
+
84
+ /**
85
+ * Registers and returns a store observer for a query, configuring Ditto to
86
+ * trigger the passed in observation handler whenever documents in the local
87
+ * store change such that the result of the matching query changes. The passed
88
+ * in query must be a `SELECT` query.
89
+ *
90
+ * Here, a function is passed as an additional argument to the observation
91
+ * handler. Call this function as soon as the observation handler is ready to
92
+ * process the the next change event. This allows the observation handler to
93
+ * control how frequently it is called. See
94
+ * {@link registerObserver | registerObserver()} for a convenience method that
95
+ * automatically signals the next invocation.
96
+ *
97
+ * The first invocation of `observationHandler` will always happen after this
98
+ * method has returned.
99
+ *
100
+ * @param query a string containing a valid query expressed in DQL.
101
+ * @param observationHandler an observation handler function that is called
102
+ * whenever the query's results change. The function is passed a
103
+ * {@link QueryResult} containing a {@link QueryResultItem} for each match.
104
+ * @param queryArguments an object of values keyed by the placeholder name
105
+ * without the leading `:`. Example: `{ "name": "Joanna" }` for a query like
106
+ * `SELECT * FROM people WHERE name = :name`.
107
+ * @returns a {@link StoreObserver} that can be used to cancel the
108
+ * observation.
109
+ * @throws {@link DittoError} `query/invalid`: if `query` argument is not a
110
+ * string or not valid DQL.
111
+ * @throws {@link DittoError} `query/arguments-invalid`: if `queryArguments`
112
+ * argument is invalid (e.g. contains unsupported types).
113
+ * @throws {@link DittoError} `query/unsupported`: if the query is not a
114
+ * `SELECT` query.
115
+ * @throws {@link DittoError} may throw other errors.
116
+ */
117
+ registerObserverWithSignalNext(query: string, observationHandler: StoreObservationHandlerWithSignalNext, queryArguments?: DQLQueryArguments): StoreObserver {
118
+ if (typeof query !== 'string') {
119
+ throw new DittoError('query/invalid', `Expected parameter 'query' to be of type 'string', found: ${typeof query}`)
120
+ }
121
+
122
+ const storeObserver = new StoreObserver(this.ditto, query, queryArguments ?? null, observationHandler)
123
+
124
+ // @ts-expect-error modifying readonly property
125
+ this.observers = Object.freeze([...this.observers, storeObserver])
126
+
127
+ // We have two requirements for this step: (1) we want to be able to wait
128
+ // for the call to FFI to finish while closing ditto and (2) we want to
129
+ // return from this function without waiting for the call to FFI to finish.
130
+ // If we would await the call here, we could end up in a situation where
131
+ // the first callback to the event handler is emitted before we return from
132
+ // the method call that started the observer.
133
+
134
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto)
135
+ this.ditto.deferCloseAsync(async () => {
136
+ return new Promise<void>((resolve) => {
137
+ step(async () => {
138
+ try {
139
+ // prettier-ignore
140
+ await mapFFIErrorsAsync(
141
+ async () => await FFI.liveQueryStart(dittoHandle.deref(), storeObserver.liveQueryID)
142
+ )
143
+ } catch (error: any) {
144
+ // As this closure executes after the surrounding method has returned we don't throw
145
+ // the error here. Instead we log the error.
146
+ Logger.error(`Failed to start live query: ${error.message}`)
147
+ }
148
+ resolve()
149
+ })
150
+ })
151
+ })
152
+
153
+ return storeObserver
154
+ }
155
+
156
+ /**
157
+ * Returns the collection for the given name. If the collection doesn't
158
+ * exist yet, it will be created automatically as soon as the first
159
+ * entry is inserted.
160
+ * A collection name is valid if:
161
+ * * its length is less than 100
162
+ * * it is not empty
163
+ * * it does not contain the char '\0'
164
+ * * it does not begin with "$TS_"
165
+ */
166
+ collection(name: string): Collection {
167
+ return new Collection(name, this)
168
+ }
169
+
170
+ /**
171
+ * Returns an object that lets you fetch or observe the collections in the
172
+ * store.
173
+ *
174
+ * @return A {@link PendingCollectionsOperation} object that you can use to
175
+ * fetch or observe the collections in the store
176
+ */
177
+ collections(): PendingCollectionsOperation {
178
+ return new PendingCollectionsOperation(this)
179
+ }
180
+
181
+ /**
182
+ * Returns the names of all available collections in the store of the
183
+ * related {@link Ditto} instance.
184
+ */
185
+ collectionNames(): Promise<string[]> {
186
+ const ditto = this.ditto
187
+ const dittoHandle = Bridge.ditto.handleFor(ditto)
188
+ return ditto.deferClose(() => {
189
+ return mapFFIErrors(() => FFI.dittoGetCollectionNames(dittoHandle.deref()))
190
+ })
191
+ }
192
+
193
+ /**
194
+ * Executes a DQL query and returns matching items as a query result.
195
+ *
196
+ * @param query a string containing a valid query expressed in DQL.
197
+ * @param queryArguments an object of values keyed by the placeholder name
198
+ * without the leading `:`. Example: `{ "name": "John" }` for a query like
199
+ * `SELECT * FROM people WHERE name = :name`.
200
+ * @returns a promise for a {@link QueryResult} containing a
201
+ * {@link QueryResultItem} for each match.
202
+ * @throws {@link DittoError} `query/invalid`: if `query` argument is not a
203
+ * string or not valid DQL.
204
+ * @throws {@link DittoError} `query/arguments-invalid`: if `queryArguments`
205
+ * argument is invalid (e.g. contains unsupported types).
206
+ * @throws {@link DittoError} may throw other errors.
207
+ */
208
+ async execute(query: string, queryArguments?: DQLQueryArguments): Promise<QueryResult> {
209
+ if (typeof query !== 'string') {
210
+ throw new DittoError('query/invalid', `Expected parameter 'query' to be of type 'string', found: ${typeof query}`)
211
+ }
212
+
213
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto)
214
+ return this.ditto.deferCloseAsync(async () => {
215
+ // A one-off query execution uses a transaction internally but a
216
+ // transaction API is not implemented yet.
217
+ const writeTransaction = null
218
+
219
+ let queryArgumentsCBOR: Uint8Array | null = null
220
+ if (queryArguments != null) {
221
+ try {
222
+ queryArgumentsCBOR = CBOR.encode(queryArguments, documentIDReplacer)
223
+ } catch (error: any) {
224
+ throw new DittoError('query/arguments-invalid', `Unable to encode query arguments: ${error.message}`)
225
+ }
226
+ }
227
+
228
+ const errorContext = { query, queryArguments }
229
+
230
+ // prettier-ignore
231
+ const responsePointer: FFI.Pointer<FFI.FFIDqlResponse> = await mapFFIErrorsAsync(
232
+ async () => await performAsyncToWorkaroundNonAsyncFFIAPI(
233
+ () => FFI.tryExperimentalExecQueryStr(dittoHandle.deref(), writeTransaction, query, queryArgumentsCBOR)
234
+ ),
235
+ undefined,
236
+ errorContext
237
+ )
238
+
239
+ return Bridge.dqlResponse.bridge(responsePointer, () => new QueryResult(responsePointer))
240
+ })
241
+ }
242
+
243
+ /**
244
+ * Initiate a write transaction in a callback.
245
+ *
246
+ * Allows you to group multiple operations together that affect multiple documents, potentially across multiple collections.
247
+ *
248
+ * @param callback is given access to a {@link WriteTransaction | write transaction object} that can be used to perform operations on the store.
249
+ * @returns a list of `WriteTransactionResult`s. There is a result for each operation performed as part of the write transaction.
250
+ */
251
+ async write(callback: (transaction: WriteTransaction) => Promise<void>): Promise<WriteTransactionResult[]> {
252
+ // Run caller's callback, rolling back if needed.
253
+ return this.ditto.deferCloseAsync(async () => {
254
+ const transaction = await WriteTransaction.init(this.ditto)
255
+
256
+ try {
257
+ await callback(transaction)
258
+ } catch (error: any) {
259
+ await transaction.rollback()
260
+ Logger.warning(`Transaction rolled back due to an error: ${error?.message}`)
261
+ throw error
262
+ }
263
+ await transaction.commit()
264
+
265
+ return transaction.results
266
+ })
267
+ }
268
+
269
+ // ----------------------------------------------------------- Internal ------
270
+
271
+ /** @internal */
272
+ constructor(ditto: Ditto) {
273
+ this.ditto = ditto
274
+ }
275
+
276
+ /**
277
+ * Registers a URL to be called whenever the given `SELECT` query observes
278
+ * changes.
279
+ *
280
+ * No validation is performed on the URL, so it is up to the caller to ensure
281
+ * that the URL is valid and can be reached.
282
+ *
283
+ * @internal
284
+ * @returns a promise for a document id that acts as a webhook id
285
+ * @throws {@link DittoError} `store/query-invalid`: if the query is invalid
286
+ * @throws {@link DittoError} `store/query-arguments-invalid`: if the query arguments
287
+ * are invalid
288
+ * @throws {@link DittoError} `store/query-unsupported`: if the query is not a
289
+ * `SELECT` query
290
+ * @throws {@link DittoError} for any other error that occurs during query execution
291
+ */
292
+ async registerObserverWebhook(query: string, url: string, queryArguments?: DQLQueryArguments): Promise<DocumentID> {
293
+ let queryArgumentsCBOR: Uint8Array | null = null
294
+ if (queryArguments != null) {
295
+ try {
296
+ queryArgumentsCBOR = CBOR.encode(queryArguments, documentIDReplacer)
297
+ } catch (error: any) {
298
+ throw new DittoError('query/arguments-invalid', `Invalid query arguments: ${error.message}`)
299
+ }
300
+ }
301
+
302
+ const errorContext = { query, queryArguments }
303
+
304
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto)
305
+ // prettier-ignore
306
+ return this.ditto.deferCloseAsync(async () => {
307
+ const webhookIDCBOR = await mapFFIErrorsAsync(
308
+ async () => await FFI.tryExperimentalWebhookRegisterDqlLiveQuery(dittoHandle.deref(), query, queryArgumentsCBOR, url),
309
+ undefined,
310
+ errorContext
311
+ )
312
+ return new DocumentID(webhookIDCBOR, true)
313
+ })
314
+ }
315
+
316
+ /**
317
+ * Unregister a store observer. No-op if the change observer has already
318
+ * been removed.
319
+ *
320
+ * This must only be called by the store observer itself.
321
+ *
322
+ * @param changeObserver the store observer to unregister
323
+ * @returns true if the store observer was found and removed, false otherwise
324
+ * @throws {@link DittoError} `internal`: if the store observer does not belong to
325
+ * this store
326
+ * @throws {@link DittoError} `internal`: if the store observer has not been
327
+ * cancelled yet
328
+ * @throws {@link DittoError} `internal`: for any other error that occurs while
329
+ * trying to unregister the store observer
330
+ * @internal
331
+ */
332
+ unregisterObserver(storeObserver: StoreObserver): boolean {
333
+ if (storeObserver.ditto !== this.ditto) {
334
+ throw new DittoError('internal', `Internal inconsistency, can't remove store observer that does not belong to this store`)
335
+ }
336
+
337
+ if (!storeObserver.isCancelled) {
338
+ throw new DittoError('internal', "Internal inconsistency, can't remove store observer that has not been cancelled")
339
+ }
340
+
341
+ // Return early if the store observer has already been removed.
342
+ const indexToDelete = this.observers.findIndex((observer) => observer === storeObserver)
343
+ if (indexToDelete === -1) {
344
+ return false
345
+ }
346
+
347
+ const newObservers = [...this.observers]
348
+ newObservers.splice(indexToDelete, 1)
349
+ // @ts-expect-error modifying readonly property
350
+ this.observers = Object.freeze(newObservers)
351
+
352
+ const dittoHandle = Bridge.ditto.handleFor(this.ditto)
353
+ this.ditto.deferClose(() => {
354
+ // prettier-ignore
355
+ mapFFIErrors(
356
+ () => FFI.liveQueryStop(dittoHandle.deref(), storeObserver.liveQueryID)
357
+ )
358
+ })
359
+ return true
360
+ }
361
+
362
+ /** @internal */
363
+ close() {
364
+ for (const observer of this.observers) {
365
+ observer.cancel()
366
+ }
367
+
368
+ // NOTE: live query webhook is taken care of by the FFI's
369
+ // `ditto_shutdown()`, no need to unregister it here.
370
+ }
371
+
372
+ /**
373
+ * Private method, used only by the Portal https://github.com/getditto/ditto/pull/3652
374
+ * @internal
375
+ */
376
+ async registerLiveQueryWebhook(collectionName: string, query: string, url: string): Promise<DocumentID> {
377
+ const ditto = this.ditto
378
+ const dittoHandle = Bridge.ditto.handleFor(ditto)
379
+ return ditto.deferCloseAsync(async () => {
380
+ const validatedQuery = validateQuery(query)
381
+ const idCBOR = await FFI.liveQueryWebhookRegister(dittoHandle.deref(), collectionName, validatedQuery, [], 0, 0, url)
382
+ return new DocumentID(idCBOR, true)
383
+ })
384
+ }
385
+ }
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright © 2023 DittoLive Incorporated. All rights reserved.
3
+ //
4
+
5
+ import * as FFI from './ffi'
6
+ import { Bridge } from './bridge'
7
+
8
+ import type { Ditto } from './ditto'
9
+ import type { Subscription } from './subscription'
10
+
11
+ /** @internal */
12
+ export type SubscriptionContextInfo = {
13
+ id: string
14
+ collectionName: string
15
+ query: string
16
+ queryArgsCBOR: Uint8Array | null
17
+ orderBys: FFI.OrderBy[]
18
+ limit: number
19
+ offset: number
20
+ }
21
+
22
+ /**
23
+ * Tracks `Subscription` instances in order to remove them when Ditto is
24
+ * closed.
25
+ *
26
+ * @internal
27
+ */
28
+ export class SubscriptionManager {
29
+ /** @internal */
30
+ constructor(ditto: Ditto) {
31
+ this.ditto = ditto
32
+ this.subscriptions = {}
33
+ this.finalizationRegistry = new FinalizationRegistry(this.removeWithContextInfo.bind(this))
34
+ }
35
+
36
+ /**
37
+ * Begin tracking a subscription instance and start it.
38
+ *
39
+ * @internal */
40
+ add(subscription: Subscription) {
41
+ const ditto = this.ditto
42
+ const dittoHandle = Bridge.ditto.handleFor(ditto)
43
+ const contextInfo = subscription.contextInfo
44
+ ditto.deferClose(async () => {
45
+ this.subscriptions[contextInfo.id] = new WeakRef(subscription)
46
+ this.finalizationRegistry.register(subscription, subscription.contextInfo, subscription)
47
+ FFI.addSubscription(dittoHandle.deref(), contextInfo.collectionName, contextInfo.query, contextInfo.queryArgsCBOR, contextInfo.orderBys, contextInfo.limit, contextInfo.offset)
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Stop tracking a subscription instance and cancel it.
53
+ *
54
+ * @internal */
55
+ remove(subscription: Subscription) {
56
+ if (this.subscriptions[subscription.contextInfo.id] == null) {
57
+ throw new Error(`Internal inconsistency, tried to remove a subscription that is not tracked: ${subscription.contextInfo.id}`)
58
+ }
59
+ this.finalizationRegistry.unregister(subscription)
60
+ this.removeWithContextInfo(subscription.contextInfo)
61
+ }
62
+
63
+ /**
64
+ * Stop tracking all subscriptions and cancel them.
65
+ *
66
+ * @internal */
67
+ close() {
68
+ this.ditto.deferClose(async () => {
69
+ for (const subscriptionID in this.subscriptions) {
70
+ const subscription = this.subscriptions[subscriptionID].deref()
71
+ if (subscription != null) {
72
+ // This doesn't call `Subscription.cancel()` because that is not
73
+ // async and we want to wait for all subscriptions to be removed.
74
+ this.remove(subscription)
75
+ }
76
+ }
77
+ })
78
+ }
79
+
80
+ // ----------------------------------------------------------- Internal ------
81
+
82
+ private ditto: Ditto
83
+ private subscriptions: { [subscriptionID: string]: WeakRef<Subscription> }
84
+ private finalizationRegistry: FinalizationRegistry<SubscriptionContextInfo>
85
+
86
+ /**
87
+ * Remove tracked subscription without unregistering from finalization
88
+ * registry.
89
+ *
90
+ * @internal */
91
+ private removeWithContextInfo(contextInfo: SubscriptionContextInfo) {
92
+ const ditto = this.ditto
93
+ const dittoHandle = Bridge.ditto.handleFor(ditto)
94
+ ditto.deferClose(() => {
95
+ delete this.subscriptions[contextInfo.id]
96
+ FFI.removeSubscription(dittoHandle.deref(), contextInfo.collectionName, contextInfo.query, contextInfo.queryArgsCBOR, contextInfo.orderBys, contextInfo.limit, contextInfo.offset)
97
+ })
98
+ }
99
+ }