@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.
- package/DittoReactNative.podspec +25 -0
- package/README.md +2 -2
- package/node/ditto.cjs.js +1 -1
- package/node/ditto.cjs.js.map +1 -0
- package/node/ditto.cjs.pretty.js +9655 -0
- package/node/ditto.cjs.pretty.js.map +1 -0
- package/node/ditto.darwin-arm64.node +0 -0
- package/node/ditto.darwin-x64.node +0 -0
- package/node/{ditto.linux-arm64.node → ditto.linux-x64.node} +0 -0
- package/node/transports.darwin-arm64.node +0 -0
- package/node/transports.darwin-x64.node +0 -0
- package/package.json +2 -1
- package/react-native/android/CMakeLists.txt +37 -0
- package/react-native/android/build.gradle +186 -0
- package/react-native/android/cpp-adapter.cpp +254 -0
- package/react-native/android/gradle.properties +5 -0
- package/react-native/android/src/main/AndroidManifest.xml +4 -0
- package/react-native/android/src/main/java/com/dittolive/rnsdk/DittoRNSDKModule.java +85 -0
- package/react-native/android/src/main/java/com/dittolive/rnsdk/DittoRNSDKPackage.java +28 -0
- package/react-native/cpp/include/Arc.hpp +141 -0
- package/react-native/cpp/include/Attachment.h +16 -0
- package/react-native/cpp/include/Authentication.h +23 -0
- package/react-native/cpp/include/Collection.h +13 -0
- package/react-native/cpp/include/DQL.h +21 -0
- package/react-native/cpp/include/Document.h +17 -0
- package/react-native/cpp/include/Identity.h +17 -0
- package/react-native/cpp/include/Lifecycle.h +17 -0
- package/react-native/cpp/include/LiveQuery.h +17 -0
- package/react-native/cpp/include/Logger.h +22 -0
- package/react-native/cpp/include/Misc.h +27 -0
- package/react-native/cpp/include/Presence.h +14 -0
- package/react-native/cpp/include/SmallPeerInfo.h +19 -0
- package/react-native/cpp/include/Transports.h +25 -0
- package/react-native/cpp/include/TypedArray.hpp +167 -0
- package/react-native/cpp/include/Utils.h +61 -0
- package/react-native/cpp/include/main.h +10 -0
- package/react-native/cpp/src/Attachment.cpp +86 -0
- package/react-native/cpp/src/Authentication.cpp +227 -0
- package/react-native/cpp/src/Collection.cpp +54 -0
- package/react-native/cpp/src/DQL.cpp +256 -0
- package/react-native/cpp/src/Document.cpp +146 -0
- package/react-native/cpp/src/Identity.cpp +123 -0
- package/react-native/cpp/src/Lifecycle.cpp +110 -0
- package/react-native/cpp/src/LiveQuery.cpp +63 -0
- package/react-native/cpp/src/Logger.cpp +200 -0
- package/react-native/cpp/src/Misc.cpp +283 -0
- package/react-native/cpp/src/Presence.cpp +79 -0
- package/react-native/cpp/src/SmallPeerInfo.cpp +142 -0
- package/react-native/cpp/src/Transports.cpp +270 -0
- package/react-native/cpp/src/TypedArray.cpp +303 -0
- package/react-native/cpp/src/Utils.cpp +138 -0
- package/react-native/cpp/src/main.cpp +152 -0
- package/react-native/dittoffi/dittoffi.h +4700 -0
- package/react-native/dittoffi/ifaddrs.cpp +385 -0
- package/react-native/dittoffi/ifaddrs.h +206 -0
- package/react-native/ios/DittoRNSDK.h +7 -0
- package/react-native/ios/DittoRNSDK.mm +107 -0
- package/react-native/ios/YeetJSIUtils.h +60 -0
- package/react-native/ios/YeetJSIUtils.mm +196 -0
- package/react-native/lib/commonjs/ditto.rn.js +92 -0
- package/react-native/lib/commonjs/ditto.rn.js.map +1 -0
- package/react-native/lib/commonjs/index.js +61 -0
- package/react-native/lib/commonjs/index.js.map +1 -0
- package/react-native/lib/module/ditto.rn.js +88 -0
- package/react-native/lib/module/ditto.rn.js.map +1 -0
- package/react-native/lib/module/index.js +27 -0
- package/react-native/lib/module/index.js.map +1 -0
- package/react-native/lib/typescript/ditto.rn.d.ts +15 -0
- package/react-native/lib/typescript/ditto.rn.d.ts.map +1 -0
- package/react-native/lib/typescript/index.d.ts +1 -0
- package/react-native/lib/typescript/index.d.ts.map +1 -0
- package/react-native/src/ditto.rn.ts +91 -0
- package/react-native/src/environment/environment.fallback.ts +4 -0
- package/react-native/src/index.ts +26 -0
- package/react-native/src/sources/@cbor-redux.ts +2 -0
- package/react-native/src/sources/@ditto.core.ts +1 -0
- package/react-native/src/sources/@environment.ts +1 -0
- package/react-native/src/sources/attachment-fetch-event.ts +54 -0
- package/react-native/src/sources/attachment-fetcher-manager.ts +144 -0
- package/react-native/src/sources/attachment-fetcher.ts +134 -0
- package/react-native/src/sources/attachment-token.ts +48 -0
- package/react-native/src/sources/attachment.ts +74 -0
- package/react-native/src/sources/augment.ts +101 -0
- package/react-native/src/sources/authenticator.ts +314 -0
- package/react-native/src/sources/base-pending-cursor-operation.ts +239 -0
- package/react-native/src/sources/base-pending-id-specific-operation.ts +109 -0
- package/react-native/src/sources/bridge.ts +553 -0
- package/react-native/src/sources/build-time-constants.ts +8 -0
- package/react-native/src/sources/cbor.ts +35 -0
- package/react-native/src/sources/collection-interface.ts +67 -0
- package/react-native/src/sources/collection.ts +212 -0
- package/react-native/src/sources/collections-event.ts +99 -0
- package/react-native/src/sources/counter.ts +82 -0
- package/react-native/src/sources/ditto.ts +979 -0
- package/react-native/src/sources/document-id.ts +159 -0
- package/react-native/src/sources/document-path.ts +306 -0
- package/react-native/src/sources/document.ts +193 -0
- package/react-native/src/sources/epilogue.ts +30 -0
- package/react-native/src/sources/error-codes.ts +52 -0
- package/react-native/src/sources/error.ts +208 -0
- package/react-native/src/sources/essentials.ts +53 -0
- package/react-native/src/sources/ffi-error.ts +122 -0
- package/react-native/src/sources/ffi.ts +2012 -0
- package/react-native/src/sources/identity.ts +163 -0
- package/react-native/src/sources/init.ts +71 -0
- package/react-native/src/sources/internal.ts +109 -0
- package/react-native/src/sources/keep-alive.ts +73 -0
- package/react-native/src/sources/key-path.ts +198 -0
- package/react-native/src/sources/live-query-event.ts +208 -0
- package/react-native/src/sources/live-query-manager.ts +102 -0
- package/react-native/src/sources/live-query.ts +166 -0
- package/react-native/src/sources/logger.ts +196 -0
- package/react-native/src/sources/main.ts +60 -0
- package/react-native/src/sources/observer-manager.ts +178 -0
- package/react-native/src/sources/observer.ts +79 -0
- package/react-native/src/sources/pending-collections-operation.ts +232 -0
- package/react-native/src/sources/pending-cursor-operation.ts +218 -0
- package/react-native/src/sources/pending-id-specific-operation.ts +218 -0
- package/react-native/src/sources/presence-manager.ts +161 -0
- package/react-native/src/sources/presence.ts +233 -0
- package/react-native/src/sources/query-result-item.ts +116 -0
- package/react-native/src/sources/query-result.ts +55 -0
- package/react-native/src/sources/register.ts +95 -0
- package/react-native/src/sources/small-peer-info.ts +177 -0
- package/react-native/src/sources/static-tcp-client.ts +6 -0
- package/react-native/src/sources/store-observer.ts +177 -0
- package/react-native/src/sources/store.ts +385 -0
- package/react-native/src/sources/subscription-manager.ts +99 -0
- package/react-native/src/sources/subscription.ts +89 -0
- package/react-native/src/sources/sync-subscription.ts +90 -0
- package/react-native/src/sources/sync.ts +559 -0
- package/react-native/src/sources/test-helpers.ts +24 -0
- package/react-native/src/sources/transport-conditions-manager.ts +104 -0
- package/react-native/src/sources/transport-config.ts +430 -0
- package/react-native/src/sources/update-result.ts +66 -0
- package/react-native/src/sources/update-results-map.ts +57 -0
- package/react-native/src/sources/websocket-client.ts +7 -0
- package/react-native/src/sources/write-transaction-collection.ts +122 -0
- package/react-native/src/sources/write-transaction-pending-cursor-operation.ts +101 -0
- package/react-native/src/sources/write-transaction-pending-id-specific-operation.ts +74 -0
- package/react-native/src/sources/write-transaction.ts +121 -0
- package/react-native.config.js +9 -0
- package/types/ditto.d.ts.map +1 -0
- package/web/ditto.es6.js +1 -1
- package/web/ditto.es6.js.map +1 -0
- package/web/ditto.es6.pretty.js +12600 -0
- package/web/ditto.es6.pretty.js.map +1 -0
- package/web/ditto.umd.js +1 -1
- package/web/ditto.umd.js.map +1 -0
- package/web/ditto.umd.pretty.js +12669 -0
- package/web/ditto.umd.pretty.js.map +1 -0
- 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
|
+
}
|