@dittolive/ditto 4.5.4 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/node/ditto.cjs.js +1111 -563
- package/node/ditto.darwin-arm64.node +0 -0
- package/node/ditto.darwin-x64.node +0 -0
- package/node/ditto.linux-arm.node +0 -0
- package/node/ditto.linux-arm64.node +0 -0
- package/node/ditto.linux-x64.node +0 -0
- package/node/ditto.win32-x64.node +0 -0
- package/node/transports.darwin-arm64.node +0 -0
- package/node/transports.darwin-x64.node +0 -0
- package/package.json +45 -1
- package/react-native/android/build.gradle +1 -1
- package/react-native/android/cpp-adapter.cpp +68 -59
- package/react-native/android/src/main/java/com/dittolive/rnsdk/DittoRNSDKModule.java +35 -0
- package/react-native/cpp/include/Arc.hpp +1 -1
- package/react-native/cpp/include/Attachment.h +5 -1
- package/react-native/cpp/include/DQL.h +9 -9
- package/react-native/cpp/include/FFIUtils.h +14 -0
- package/react-native/cpp/include/IO.h +13 -0
- package/react-native/cpp/include/Lifecycle.h +0 -1
- package/react-native/cpp/include/Misc.h +3 -0
- package/react-native/cpp/include/Utils.h +0 -2
- package/react-native/cpp/src/Attachment.cpp +200 -13
- package/react-native/cpp/src/Authentication.cpp +3 -3
- package/react-native/cpp/src/DQL.cpp +23 -23
- package/react-native/cpp/src/Document.cpp +10 -10
- package/react-native/cpp/src/FFIUtils.cpp +64 -0
- package/react-native/cpp/src/IO.cpp +35 -0
- package/react-native/cpp/src/Identity.cpp +3 -3
- package/react-native/cpp/src/Lifecycle.cpp +2 -19
- package/react-native/cpp/src/LiveQuery.cpp +3 -5
- package/react-native/cpp/src/Logger.cpp +171 -172
- package/react-native/cpp/src/Misc.cpp +52 -4
- package/react-native/cpp/src/Presence.cpp +1 -1
- package/react-native/cpp/src/SmallPeerInfo.cpp +1 -1
- package/react-native/cpp/src/Transports.cpp +10 -5
- package/react-native/cpp/src/Utils.cpp +110 -114
- package/react-native/cpp/src/main.cpp +28 -15
- package/react-native/dittoffi/dittoffi.h +328 -280
- package/react-native/ios/DittoRNSDK.mm +123 -71
- package/react-native/src/ditto.rn.ts +30 -6
- package/react-native/src/index.ts +7 -4
- package/react-native/src/sources/@cbor-redux.ts +1 -1
- package/react-native/src/sources/attachment-fetch-event.ts +2 -2
- package/react-native/src/sources/attachment-fetcher-manager.ts +5 -4
- package/react-native/src/sources/attachment-fetcher.ts +152 -21
- package/react-native/src/sources/attachment-token.ts +94 -13
- package/react-native/src/sources/attachment.ts +66 -19
- package/react-native/src/sources/augment.ts +13 -6
- package/react-native/src/sources/base-pending-cursor-operation.ts +22 -6
- package/react-native/src/sources/base-pending-id-specific-operation.ts +3 -0
- package/react-native/src/sources/bridge.ts +2 -2
- package/react-native/src/sources/cbor.ts +0 -15
- package/react-native/src/sources/collection-interface.ts +12 -6
- package/react-native/src/sources/collection.ts +9 -2
- package/react-native/src/sources/ditto.ts +26 -18
- package/react-native/src/sources/document-id.ts +11 -7
- package/react-native/src/sources/document-path.ts +4 -2
- package/react-native/src/sources/document.ts +49 -5
- package/react-native/src/sources/error-codes.ts +28 -0
- package/react-native/src/sources/error.ts +20 -1
- package/react-native/src/sources/essentials.ts +25 -3
- package/react-native/src/sources/ffi-error.ts +2 -1
- package/react-native/src/sources/ffi.ts +180 -102
- package/react-native/src/sources/internal.ts +37 -3
- package/react-native/src/sources/live-query-manager.ts +10 -1
- package/react-native/src/sources/live-query.ts +1 -1
- package/react-native/src/sources/observer-manager.ts +7 -0
- package/react-native/src/sources/pending-id-specific-operation.ts +2 -2
- package/react-native/src/sources/presence-manager.ts +12 -2
- package/react-native/src/sources/presence.ts +5 -0
- package/react-native/src/sources/query-result-item.ts +15 -0
- package/react-native/src/sources/small-peer-info.ts +2 -2
- package/react-native/src/sources/static-tcp-client.ts +2 -0
- package/react-native/src/sources/store-observer.ts +4 -2
- package/react-native/src/sources/store.ts +253 -3
- package/react-native/src/sources/sync.ts +6 -3
- package/react-native/src/sources/transport-config.ts +2 -2
- package/react-native/src/sources/update-results-map.ts +8 -0
- package/react-native/src/sources/write-transaction-collection.ts +1 -1
- package/react-native/src/sources/write-transaction.ts +1 -1
- package/types/ditto.d.ts +2866 -2568
- package/web/ditto.es6.js +1 -1
- package/web/ditto.umd.js +1 -1
- package/web/ditto.wasm +0 -0
- package/react-native/.yarn/install-state.gz +0 -0
- package/react-native/.yarnrc.yml +0 -1
|
@@ -3,24 +3,68 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import * as FFI from './ffi'
|
|
6
|
+
import { mapFFIErrors } from './error'
|
|
7
|
+
|
|
8
|
+
import type { AttachmentMetadata } from './attachment'
|
|
9
|
+
import type { Store } from './store'
|
|
10
|
+
|
|
11
|
+
/** @internal */
|
|
12
|
+
export type UntypedAttachmentToken = { id: string; len: number | BigInt; metadata: AttachmentMetadata }
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
export type TypedAttachmentToken = { [FFI.DittoCRDTTypeKey]?: FFI.DittoCRDTType.attachment; _id: Uint8Array; _len: number | BigInt; _meta: AttachmentMetadata }
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* Serves as a token for a specific attachment that you can pass to a call to
|
|
9
|
-
* {@link
|
|
10
|
-
* {@link Collection}.
|
|
19
|
+
* {@link Store.fetchAttachment | ditto.store.fetchAttachment()}.
|
|
11
20
|
*/
|
|
12
21
|
export class AttachmentToken {
|
|
13
|
-
/**
|
|
14
|
-
|
|
22
|
+
/** The attachment's ID. */
|
|
23
|
+
// This ID is a _non-padded_ base64-encoded version of `idBytes`.
|
|
24
|
+
readonly id: string
|
|
15
25
|
|
|
16
|
-
/**
|
|
17
|
-
readonly len: number
|
|
26
|
+
/** The attachment's size given as number of bytes. */
|
|
27
|
+
readonly len: number | BigInt
|
|
28
|
+
|
|
29
|
+
/** The attachment's metadata. */
|
|
30
|
+
readonly metadata: AttachmentMetadata
|
|
31
|
+
|
|
32
|
+
// -------------------------------------------------------------------------
|
|
18
33
|
|
|
19
34
|
/** @internal */
|
|
20
|
-
|
|
35
|
+
constructor(jsObj: UntypedAttachmentToken | TypedAttachmentToken) {
|
|
36
|
+
// There are two representations of attachment tokens:
|
|
37
|
+
// 1. The legacy typed representation is an internal format that is used by
|
|
38
|
+
// the query builder API. It can be identified by the presence of the
|
|
39
|
+
// [FFI.DittoCRDTTypeKey] field.
|
|
40
|
+
// 2. The untyped representation is used in our public API and was first
|
|
41
|
+
// introduced in the HTTP API. It is now used by the DQL API as well. It
|
|
42
|
+
// uses a non-padded base64-encoded ID.
|
|
43
|
+
let id: Uint8Array, len: number | BigInt, meta: AttachmentMetadata
|
|
44
|
+
if (jsObj[FFI.DittoCRDTTypeKey] != null) {
|
|
45
|
+
;({ id, len, meta } = AttachmentToken.validateTypedInput(jsObj as TypedAttachmentToken))
|
|
46
|
+
} else {
|
|
47
|
+
;({ id, len, meta } = AttachmentToken.validateUntypedInput(jsObj as UntypedAttachmentToken))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// base64-encode string and remove padding
|
|
51
|
+
this.id = mapFFIErrors(() => FFI.base64encode(id, 'Unpadded'))
|
|
52
|
+
this.idBytes = id
|
|
53
|
+
this.len = len
|
|
54
|
+
this.metadata = meta
|
|
55
|
+
}
|
|
21
56
|
|
|
22
57
|
/** @internal */
|
|
23
|
-
|
|
58
|
+
readonly idBytes: Uint8Array
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate an input value that has a field `[FFI.DittoCRDTTypeKey]` and
|
|
62
|
+
* return its contents.
|
|
63
|
+
*
|
|
64
|
+
* @throws {Error} If the input is invalid.
|
|
65
|
+
* @returns {object} binary id, len and metadata of the attachment token
|
|
66
|
+
*/
|
|
67
|
+
private static validateTypedInput(jsObj: TypedAttachmentToken): { id: Uint8Array; len: number | BigInt; meta: AttachmentMetadata } {
|
|
24
68
|
const type = jsObj[FFI.DittoCRDTTypeKey]
|
|
25
69
|
if (type !== FFI.DittoCRDTType.attachment) {
|
|
26
70
|
throw new Error('Invalid attachment token')
|
|
@@ -32,8 +76,8 @@ export class AttachmentToken {
|
|
|
32
76
|
}
|
|
33
77
|
|
|
34
78
|
const len = jsObj['_len']
|
|
35
|
-
if (typeof len !== 'number' || len < 0) {
|
|
36
|
-
throw new Error('Invalid attachment token length')
|
|
79
|
+
if ((typeof len !== 'number' && typeof len !== 'bigint') || len < 0) {
|
|
80
|
+
throw new Error('Invalid attachment token length, must be a non-negative number or bigint')
|
|
37
81
|
}
|
|
38
82
|
|
|
39
83
|
const meta = jsObj['_meta']
|
|
@@ -41,8 +85,45 @@ export class AttachmentToken {
|
|
|
41
85
|
throw new Error('Invalid attachment token meta')
|
|
42
86
|
}
|
|
43
87
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
return { id, len, meta }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate an untyped input value and return its contents.
|
|
93
|
+
*
|
|
94
|
+
* Converts _unpadded_ base64-encoded ID in input to _padded_ base64-encoded
|
|
95
|
+
* ID before returning it as `Uint8Array`.
|
|
96
|
+
*
|
|
97
|
+
* @throws {@link DittoError} `store/attachment-token-invalid` If the input id
|
|
98
|
+
* is not a valid base64 string.
|
|
99
|
+
* @returns {object} binary id, len and metadata of the attachment token
|
|
100
|
+
*/
|
|
101
|
+
private static validateUntypedInput(jsObj: UntypedAttachmentToken): { id: Uint8Array; len: number | BigInt; meta: AttachmentMetadata } {
|
|
102
|
+
const idBase64 = jsObj['id']
|
|
103
|
+
if (typeof idBase64 !== 'string') {
|
|
104
|
+
throw new Error('Invalid attachment token id')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const id = mapFFIErrors(
|
|
108
|
+
() => FFI.tryBase64Decode(idBase64, 'Unpadded'),
|
|
109
|
+
{
|
|
110
|
+
Base64Invalid: ['store/attachment-token-invalid', 'Failed to decode attachment token id from base64 input'],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
attachmentTokenID: idBase64,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const len = jsObj['len']
|
|
118
|
+
if ((typeof len !== 'number' && typeof len !== 'bigint') || len < 0) {
|
|
119
|
+
throw new Error('Invalid attachment token length, must be a non-negative number or bigint')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const meta = jsObj['metadata']
|
|
123
|
+
if (typeof meta !== 'object') {
|
|
124
|
+
throw new Error('Invalid attachment token meta')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { id, len, meta }
|
|
47
128
|
}
|
|
48
129
|
}
|
|
@@ -3,17 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import * as FFI from './ffi'
|
|
6
|
-
import { Bridge } from './bridge'
|
|
7
6
|
import * as Environment from './@environment'
|
|
7
|
+
import { Bridge } from './bridge'
|
|
8
|
+
import { DittoError } from './error'
|
|
8
9
|
|
|
9
10
|
import type { Ditto } from './ditto'
|
|
10
11
|
import type { AttachmentToken } from './attachment-token'
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A key-value map of user-defined metadata for an attachment.
|
|
15
|
+
*/
|
|
16
|
+
export type AttachmentMetadata = { [key: string]: string }
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* Represents an attachment and can be used to insert the associated attachment
|
|
14
20
|
* into a document at a specific key-path. You can't instantiate an attachment
|
|
15
|
-
* directly, please use the
|
|
16
|
-
*
|
|
21
|
+
* directly, please use the
|
|
22
|
+
* {@link Store.newAttachment | ditto.store.newAttachment()} method instead.
|
|
17
23
|
*/
|
|
18
24
|
export class Attachment {
|
|
19
25
|
/** @internal */
|
|
@@ -22,6 +28,16 @@ export class Attachment {
|
|
|
22
28
|
/** @internal */
|
|
23
29
|
readonly token: AttachmentToken
|
|
24
30
|
|
|
31
|
+
/** The attachment's ID. */
|
|
32
|
+
get id(): string {
|
|
33
|
+
return this.token.id
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The attachment's size given as number of bytes. */
|
|
37
|
+
get len(): number | BigInt {
|
|
38
|
+
return this.token.len
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
/** The attachment's metadata. */
|
|
26
42
|
get metadata(): { [key: string]: string } {
|
|
27
43
|
return this.token.metadata
|
|
@@ -30,24 +46,29 @@ export class Attachment {
|
|
|
30
46
|
/**
|
|
31
47
|
* Returns the attachment's data.
|
|
32
48
|
*/
|
|
33
|
-
|
|
49
|
+
data(): Promise<Uint8Array> {
|
|
34
50
|
const ditto = this.ditto
|
|
35
51
|
const dittoHandle = Bridge.ditto.handleFor(ditto)
|
|
36
52
|
return this.ditto.deferCloseAsync(async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// const attachmentPath = FFI.dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref())
|
|
45
|
-
// const fs = require('fs').promises
|
|
46
|
-
// return await fs.readFile(attachmentPath)
|
|
47
|
-
// }
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if (Environment.isReactNativeBuild) {
|
|
56
|
+
const attachmentHandle = Bridge.attachment.handleFor(this)
|
|
57
|
+
const attachmentPath = FFI.dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref())
|
|
58
|
+
return await FFI.readFile(attachmentPath)
|
|
59
|
+
}
|
|
48
60
|
})
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Returns the attachment's data.
|
|
65
|
+
*
|
|
66
|
+
* @deprecated Use `data()` instead.
|
|
67
|
+
*/
|
|
68
|
+
getData(): Promise<Uint8Array> {
|
|
69
|
+
return this.data()
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
/**
|
|
52
73
|
* Copies the attachment to the specified file path. Node-only,
|
|
53
74
|
* throws in the browser.
|
|
@@ -58,11 +79,14 @@ export class Attachment {
|
|
|
58
79
|
const ditto = this.ditto
|
|
59
80
|
const dittoHandle = Bridge.ditto.handleFor(ditto)
|
|
60
81
|
return this.ditto.deferCloseAsync(async () => {
|
|
61
|
-
if (Environment.isWebBuild) {
|
|
62
|
-
throw new Error(`Can't copy attachment to path, not available when running in the browser.`)
|
|
63
|
-
}
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
if (Environment.isReactNativeBuild) {
|
|
84
|
+
const attachmentHandle = Bridge.attachment.handleFor(this)
|
|
85
|
+
const attachmentPath = FFI.dittoGetCompleteAttachmentPath(dittoHandle.deref(), attachmentHandle.deref())
|
|
86
|
+
// If the file already exists, we fail. This is the same behavior as
|
|
87
|
+
// for the Swift/ObjC SDK.
|
|
88
|
+
return await FFI.copyFile(attachmentPath, path, ditto.persistenceDirectory)
|
|
89
|
+
}
|
|
66
90
|
})
|
|
67
91
|
}
|
|
68
92
|
|
|
@@ -72,3 +96,26 @@ export class Attachment {
|
|
|
72
96
|
this.token = token
|
|
73
97
|
}
|
|
74
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validates the given attachment metadata. Metadata must be a flat object with
|
|
102
|
+
* string values.
|
|
103
|
+
*
|
|
104
|
+
* This should really happen in core to make sure we use the same validation
|
|
105
|
+
* logic across SDKs but we decided to postpone that for the next iteration on
|
|
106
|
+
* attachments.
|
|
107
|
+
*
|
|
108
|
+
* @throws {@link DittoError} 'store/failed-to-create-attachment'
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
export function validateAttachmentMetadata(metadata: AttachmentMetadata): void {
|
|
112
|
+
if (typeof metadata !== 'object') {
|
|
113
|
+
throw new DittoError('store/failed-to-create-attachment', `Invalid attachment metadata: expected a value of type object but got ${typeof metadata}.`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const key in metadata) {
|
|
117
|
+
if (typeof metadata[key] !== 'string') {
|
|
118
|
+
throw new DittoError('store/failed-to-create-attachment', `Invalid attachment metadata: metadata values must be strings but key '${key}' has a value of type ${typeof metadata[key]}.`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -46,11 +46,16 @@ export function augmentJSONValue(json, mutDoc, workingPath) {
|
|
|
46
46
|
* Converts objects that may contain instances of classes of this SDK, i.e.
|
|
47
47
|
* `DocumentID`, `Counter`, `Register` and `Attachment`, into plain JS objects
|
|
48
48
|
* that can be passed to the FFI layer.
|
|
49
|
+
*
|
|
50
|
+
* WARNING: For attachments in the input, the output can contain `BigInt`
|
|
51
|
+
* values, which are not JSON-compatible.
|
|
52
|
+
*
|
|
53
|
+
* @throws {Error} If `jsObj` contains a non-finite float value.
|
|
49
54
|
*/
|
|
50
|
-
export function desugarJSObject(jsObj: any
|
|
55
|
+
export function desugarJSObject(jsObj: any): any {
|
|
51
56
|
if (jsObj && typeof jsObj === 'object') {
|
|
52
57
|
if (Array.isArray(jsObj)) {
|
|
53
|
-
return jsObj.map((v, idx) => desugarJSObject(v
|
|
58
|
+
return jsObj.map((v, idx) => desugarJSObject(v))
|
|
54
59
|
} else if (jsObj instanceof DocumentID) {
|
|
55
60
|
return jsObj.value
|
|
56
61
|
} else if (jsObj instanceof Counter) {
|
|
@@ -65,17 +70,19 @@ export function desugarJSObject(jsObj: any, atRoot: boolean = false): any {
|
|
|
65
70
|
return registerJSON
|
|
66
71
|
} else if (jsObj instanceof Attachment) {
|
|
67
72
|
const attachmentJSON = {
|
|
68
|
-
_id: jsObj.token.
|
|
69
|
-
_len: jsObj.token.len,
|
|
73
|
+
_id: jsObj.token.idBytes,
|
|
74
|
+
_len: jsObj.token.len, // may be a BigInt
|
|
70
75
|
_meta: jsObj.token.metadata,
|
|
71
76
|
}
|
|
72
77
|
attachmentJSON[FFI.DittoCRDTTypeKey] = FFI.DittoCRDTType.attachment
|
|
73
78
|
return attachmentJSON
|
|
74
79
|
} else {
|
|
80
|
+
// Create a copy to not mutate the original object
|
|
81
|
+
const jsObjJSON = {}
|
|
75
82
|
for (const [key, value] of Object.entries(jsObj)) {
|
|
76
|
-
|
|
83
|
+
jsObjJSON[key] = desugarJSObject(value)
|
|
77
84
|
}
|
|
78
|
-
return
|
|
85
|
+
return jsObjJSON
|
|
79
86
|
}
|
|
80
87
|
} else {
|
|
81
88
|
checkForUnsupportedValues(jsObj)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as FFI from './ffi'
|
|
6
6
|
|
|
7
|
+
import { desugarJSObject } from './augment'
|
|
7
8
|
import { Document, MutableDocument } from './document'
|
|
8
9
|
import { UpdateResultsMap } from './update-results-map'
|
|
9
10
|
import { CBOR } from './cbor'
|
|
@@ -39,6 +40,9 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
|
|
|
39
40
|
* Updates documents that match the query generated by the preceding function
|
|
40
41
|
* chaining.
|
|
41
42
|
*
|
|
43
|
+
* Document values must not be set to any non-finite numbers (`NaN`,
|
|
44
|
+
* `Infinity`, `-Infinity`).
|
|
45
|
+
*
|
|
42
46
|
* @param closure A closure that gets called with all of the documents
|
|
43
47
|
* matching the query. The documents are instances of {@link MutableDocument}
|
|
44
48
|
* so you can call update-related functions on them.
|
|
@@ -64,18 +68,20 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
|
|
|
64
68
|
* Sorts the documents that match the query provided in the preceding
|
|
65
69
|
* `find`-like function call.
|
|
66
70
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
71
|
+
* Documents that are missing the field to sort by will appear at
|
|
72
|
+
* the beginning of the results when sorting in ascending order.
|
|
73
|
+
*
|
|
74
|
+
* @param query Name or path of the field to sort by.
|
|
69
75
|
*
|
|
70
76
|
* @param direction Specify whether you want the sorting order to be
|
|
71
|
-
* `
|
|
77
|
+
* `ascending` or `descending`. Defaults to `ascending`.
|
|
72
78
|
*
|
|
73
79
|
* @return A cursor that you can chain further function calls and then either
|
|
74
80
|
* get the matching documents immediately or get updates about them over time.
|
|
75
81
|
*/
|
|
76
|
-
sort(
|
|
82
|
+
sort(query: string, direction: SortDirection = 'ascending'): BasePendingCursorOperation {
|
|
77
83
|
this.orderBys.push({
|
|
78
|
-
query
|
|
84
|
+
query,
|
|
79
85
|
direction: direction === 'ascending' ? 'Ascending' : 'Descending',
|
|
80
86
|
})
|
|
81
87
|
return this
|
|
@@ -97,6 +103,8 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
|
|
|
97
103
|
* get the matching documents immediately or get updates about them over time.
|
|
98
104
|
*/
|
|
99
105
|
offset(offset: number): BasePendingCursorOperation {
|
|
106
|
+
// REFACTOR: factor out parameter validation.
|
|
107
|
+
|
|
100
108
|
if (offset < 0) throw new Error(`Can't offset by '${offset}', offset must be >= 0`)
|
|
101
109
|
if (!Number.isFinite(offset)) throw new Error(`Can't offset by '${offset}', offset must be a finite number`)
|
|
102
110
|
if (Number.isNaN(offset)) throw new Error(`Can't offset by '${offset}', offset must be a valid number`)
|
|
@@ -118,6 +126,8 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
|
|
|
118
126
|
* get the matching documents immediately or get updates about them over time.
|
|
119
127
|
*/
|
|
120
128
|
limit(limit: number): BasePendingCursorOperation {
|
|
129
|
+
// REFACTOR: factor out parameter validation.
|
|
130
|
+
|
|
121
131
|
if (limit < -1) throw new Error(`Can't limit to '${limit}', limit must be >= -1 (where -1 means unlimited)`)
|
|
122
132
|
if (!Number.isFinite(limit)) throw new Error(`Can't limit to '${limit}', limit must be a finite number`)
|
|
123
133
|
if (Number.isNaN(limit)) throw new Error(`Can't limit to '${limit}', limit must be a valid number`)
|
|
@@ -215,7 +225,13 @@ export abstract class BasePendingCursorOperation implements PromiseLike<Document
|
|
|
215
225
|
this.query = validateQuery(query)
|
|
216
226
|
this.queryArgs = queryArgs ? Object.freeze({ ...queryArgs }) : null
|
|
217
227
|
this.collection = collection
|
|
218
|
-
|
|
228
|
+
|
|
229
|
+
if (queryArgs == null) {
|
|
230
|
+
this.queryArgsCBOR = null
|
|
231
|
+
} else {
|
|
232
|
+
const queryArgsJSON = desugarJSObject(queryArgs)
|
|
233
|
+
this.queryArgsCBOR = CBOR.encode(queryArgsJSON)
|
|
234
|
+
}
|
|
219
235
|
}
|
|
220
236
|
|
|
221
237
|
/** @internal */
|
|
@@ -42,6 +42,9 @@ export abstract class BasePendingIDSpecificOperation implements PromiseLike<Docu
|
|
|
42
42
|
/**
|
|
43
43
|
* Updates the document with the matching ID.
|
|
44
44
|
*
|
|
45
|
+
* Document values must not be set to any non-finite numbers (`NaN`,
|
|
46
|
+
* `Infinity`, `-Infinity`).
|
|
47
|
+
*
|
|
45
48
|
* @param closure A closure that gets called with the document matching the
|
|
46
49
|
* ID. If found, the document is a {@link MutableDocument}, so you can call
|
|
47
50
|
* update-related functions on it. If the document is not found then the value
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import * as FFI from './ffi'
|
|
6
6
|
|
|
7
7
|
import { Logger } from './logger'
|
|
8
|
-
import { QueryResult } from './query-result'
|
|
9
|
-
import { QueryResultItem } from './query-result-item'
|
|
10
8
|
|
|
11
9
|
import type { Document, MutableDocument } from './document'
|
|
12
10
|
import type { Attachment } from './attachment'
|
|
11
|
+
import type { QueryResult } from './query-result'
|
|
12
|
+
import type { QueryResultItem } from './query-result-item'
|
|
13
13
|
|
|
14
14
|
import type { StaticTCPClient } from './static-tcp-client'
|
|
15
15
|
import type { WebsocketClient } from './websocket-client'
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { CBOR as CBORRedux } from './@cbor-redux'
|
|
6
6
|
|
|
7
|
-
import { DocumentID } from './document-id'
|
|
8
|
-
|
|
9
7
|
/** @internal */
|
|
10
8
|
export class CBOR {
|
|
11
9
|
/** @internal */
|
|
@@ -20,16 +18,3 @@ export class CBOR {
|
|
|
20
18
|
return CBORRedux.decode(arrayBuffer, reviver)
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Custom replacer that converts `DocumentID` instances to their string
|
|
26
|
-
* representation.
|
|
27
|
-
*
|
|
28
|
-
* @internal
|
|
29
|
-
*/
|
|
30
|
-
export function documentIDReplacer(key: any, value: any): any {
|
|
31
|
-
if (value instanceof DocumentID) {
|
|
32
|
-
return value.toString()
|
|
33
|
-
}
|
|
34
|
-
return value
|
|
35
|
-
}
|
|
@@ -10,6 +10,10 @@ import type { BasePendingCursorOperation } from './base-pending-cursor-operation
|
|
|
10
10
|
import type { BasePendingIDSpecificOperation } from './base-pending-id-specific-operation'
|
|
11
11
|
|
|
12
12
|
export type UpsertOptions = {
|
|
13
|
+
/**
|
|
14
|
+
* Specifies the desired strategy for inserting a document. The default
|
|
15
|
+
* strategy is `merge`. See {@link WriteStrategy} for more information.
|
|
16
|
+
*/
|
|
13
17
|
writeStrategy?: WriteStrategy
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -55,13 +59,15 @@ export interface CollectionInterface {
|
|
|
55
59
|
findByIDCBOR(idCBOR: Uint8Array): BasePendingIDSpecificOperation
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
* Inserts a new document into the collection and returns its ID.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
62
|
+
* Inserts a new document into the collection and returns its ID.
|
|
63
|
+
*
|
|
64
|
+
* If the document already exists, the contents of both are merged by default.
|
|
65
|
+
* You can change this by providing a different `writeStrategy` via `options`.
|
|
61
66
|
*
|
|
62
|
-
* @param value The content of the document to be inserted or updated.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
67
|
+
* @param value The content of the document to be inserted or updated. Must
|
|
68
|
+
* not contain any non-finite numbers (NaN, Infinity, -Infinity).
|
|
69
|
+
* @param options Change defaults for the behavior of the operation, such as
|
|
70
|
+
* the write strategy.
|
|
65
71
|
*/
|
|
66
72
|
upsert(value: DocumentValue, options: UpsertOptions): Promise<DocumentID>
|
|
67
73
|
}
|
|
@@ -18,6 +18,7 @@ import { PendingIDSpecificOperation } from './pending-id-specific-operation'
|
|
|
18
18
|
import { performAsyncToWorkaroundNonAsyncFFIAPI } from './internal'
|
|
19
19
|
import { desugarJSObject } from './augment'
|
|
20
20
|
|
|
21
|
+
import type { TypedAttachmentToken } from './attachment-token'
|
|
21
22
|
import type { Store } from './store'
|
|
22
23
|
import type { DocumentValue } from './document'
|
|
23
24
|
import type { DocumentIDValue } from './document-id'
|
|
@@ -94,7 +95,7 @@ export class Collection implements CollectionInterface {
|
|
|
94
95
|
return ditto.deferCloseAsync(async () => {
|
|
95
96
|
const writeStrategy = options.writeStrategy ?? 'merge'
|
|
96
97
|
|
|
97
|
-
const documentValueJSON = desugarJSObject(value
|
|
98
|
+
const documentValueJSON = desugarJSObject(value)
|
|
98
99
|
const documentValueCBOR = CBOR.encode(documentValueJSON)
|
|
99
100
|
|
|
100
101
|
const idCBOR = await performAsyncToWorkaroundNonAsyncFFIAPI(async () => {
|
|
@@ -130,6 +131,9 @@ export class Collection implements CollectionInterface {
|
|
|
130
131
|
* attachment with or the raw data.
|
|
131
132
|
*
|
|
132
133
|
* @param metadata Metadata relating to the attachment.
|
|
134
|
+
*
|
|
135
|
+
* @deprecated Use {@link Store.newAttachment | ditto.store.newAttachment() }
|
|
136
|
+
* instead.
|
|
133
137
|
*/
|
|
134
138
|
async newAttachment(pathOrData: string | Uint8Array, metadata: { [key: string]: string } = {}): Promise<Attachment> {
|
|
135
139
|
const ditto = this.store.ditto
|
|
@@ -157,7 +161,7 @@ export class Collection implements CollectionInterface {
|
|
|
157
161
|
const attachmentTokenJSON = { _id: id, _len: len, _meta: { ...metadata } }
|
|
158
162
|
attachmentTokenJSON[FFI.DittoCRDTTypeKey] = FFI.DittoCRDTType.attachment
|
|
159
163
|
|
|
160
|
-
const attachmentToken = new AttachmentToken(attachmentTokenJSON)
|
|
164
|
+
const attachmentToken = new AttachmentToken(attachmentTokenJSON as TypedAttachmentToken)
|
|
161
165
|
const attachment = new Attachment(ditto, attachmentToken)
|
|
162
166
|
|
|
163
167
|
return Bridge.attachment.bridge(handle, () => attachment)
|
|
@@ -187,6 +191,9 @@ export class Collection implements CollectionInterface {
|
|
|
187
191
|
* @return An `AttachmentFetcher` object, which must be kept alive for the
|
|
188
192
|
* fetch request to proceed and for you to be notified about the attachment's
|
|
189
193
|
* fetch status changes.
|
|
194
|
+
*
|
|
195
|
+
* @deprecated Use
|
|
196
|
+
* {@link Store.fetchAttachment | ditto.store.fetchAttachment() } instead.
|
|
190
197
|
*/
|
|
191
198
|
fetchAttachment(token: AttachmentToken, eventHandler?: (event: AttachmentFetchEvent) => void): AttachmentFetcher {
|
|
192
199
|
if (token == null || !(token instanceof AttachmentToken)) {
|
|
@@ -217,9 +217,7 @@ export class Ditto {
|
|
|
217
217
|
|
|
218
218
|
// Check if device name stays the same on sdk and core levels (#10729).
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
this.deviceName = navigator.userAgent
|
|
222
|
-
}
|
|
220
|
+
|
|
223
221
|
|
|
224
222
|
if (Environment.isReactNativeBuild) {
|
|
225
223
|
this.deviceName = FFI.getDeviceName() as string
|
|
@@ -227,16 +225,12 @@ export class Ditto {
|
|
|
227
225
|
|
|
228
226
|
this.keepAlive = new KeepAlive()
|
|
229
227
|
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// newly created or to the stored one if already persisted.
|
|
237
|
-
|
|
238
|
-
const uninitializedDittoX = FFI.uninitializedDittoMake(this.persistenceDirectory)
|
|
239
|
-
|
|
228
|
+
// WORKAROUND: the login provider triggers the registered callback right
|
|
229
|
+
// when the auth client is being constructed. At this point, we don't have
|
|
230
|
+
// the auth client, which would be needed to perform a login, nor did we
|
|
231
|
+
// have a chance to create an authenticator. Therefore catch that first
|
|
232
|
+
// callback, store the seconds remaining and proceed with propagating
|
|
233
|
+
// it after all pieces are in place.
|
|
240
234
|
let secondsRemainingUntilAuthenticationExpires: number | null = null
|
|
241
235
|
|
|
242
236
|
const weakThis = new WeakRef(this)
|
|
@@ -275,7 +269,10 @@ export class Ditto {
|
|
|
275
269
|
throw new Error(`Can't create Ditto, unsupported identity type: ${validIdentity}`)
|
|
276
270
|
})()
|
|
277
271
|
|
|
278
|
-
|
|
272
|
+
// History tracking is an experimental feature that is not supported in the JS SDK.
|
|
273
|
+
const historyTracking = 'Disabled'
|
|
274
|
+
|
|
275
|
+
const dittoPointer = FFI.dittoMake(this.persistenceDirectory, identityConfig, historyTracking)
|
|
279
276
|
|
|
280
277
|
FFI.dittoAuthClientSetValidityListener(dittoPointer, function (...args) {
|
|
281
278
|
const ditto = weakThis.deref()
|
|
@@ -294,6 +291,10 @@ export class Ditto {
|
|
|
294
291
|
|
|
295
292
|
Bridge.ditto.bridge(dittoPointer, this)
|
|
296
293
|
|
|
294
|
+
if (Environment.isReactNativeBuild) {
|
|
295
|
+
this.disableSyncWithV3()
|
|
296
|
+
}
|
|
297
|
+
|
|
297
298
|
// IMPORTANT: Keeping the auth client around accumulates run-times and
|
|
298
299
|
// resources which becomes a problem specifically in tests (where we use one
|
|
299
300
|
// Ditto instance per test). We therefore keep it only if needed, i.e.
|
|
@@ -312,6 +313,8 @@ export class Ditto {
|
|
|
312
313
|
if (strongThis.auth) {
|
|
313
314
|
strongThis.auth['@ditto.authenticationExpiring'](secondsRemaining)
|
|
314
315
|
} else {
|
|
316
|
+
// WORKAROUND: see description above where the
|
|
317
|
+
// secondsRemainingUntilAuthenticationExpires variable is declared.
|
|
315
318
|
secondsRemainingUntilAuthenticationExpires = secondsRemaining
|
|
316
319
|
}
|
|
317
320
|
})
|
|
@@ -364,6 +367,8 @@ export class Ditto {
|
|
|
364
367
|
|
|
365
368
|
disableDeadlockTimeoutWhenDebugging()
|
|
366
369
|
|
|
370
|
+
// WORKAROUND: see description above where the
|
|
371
|
+
// secondsRemainingUntilAuthenticationExpires variable is declared.
|
|
367
372
|
if (secondsRemainingUntilAuthenticationExpires != null) {
|
|
368
373
|
this.auth['@ditto.authenticationExpiring'](secondsRemainingUntilAuthenticationExpires)
|
|
369
374
|
}
|
|
@@ -468,6 +473,7 @@ export class Ditto {
|
|
|
468
473
|
validatedPath = path
|
|
469
474
|
}
|
|
470
475
|
|
|
476
|
+
|
|
471
477
|
if (Environment.isReactNativeBuild) {
|
|
472
478
|
validatedPath = FFI.createDirectory(validatedPath) as string
|
|
473
479
|
}
|
|
@@ -666,9 +672,6 @@ export class Ditto {
|
|
|
666
672
|
* @throws {Error} if called in a React Native environment.
|
|
667
673
|
*/
|
|
668
674
|
async disableSyncWithV3(): Promise<void> {
|
|
669
|
-
if (Environment.isReactNativeBuild) {
|
|
670
|
-
throw new Error('Disabling sync with V3 is not supported in a React Native environment.')
|
|
671
|
-
}
|
|
672
675
|
const dittoHandle = Bridge.ditto.handleFor(this)
|
|
673
676
|
return this.deferCloseAsync(async () => {
|
|
674
677
|
await FFI.dittoDisableSyncWithV3(dittoHandle.deref())
|
|
@@ -708,7 +711,11 @@ export class Ditto {
|
|
|
708
711
|
// ignored because they are handled at the original call site.
|
|
709
712
|
do {
|
|
710
713
|
await Promise.allSettled(this.pendingOperations)
|
|
711
|
-
|
|
714
|
+
// REFACTOR: in theory, we could end up in an endless loop here if for
|
|
715
|
+
// some reason a resolved or rejected promise isn't removed from
|
|
716
|
+
// `pendingOperations`. AFAICS, this is not possible atm due to the
|
|
717
|
+
// way `deferClose` and `deferCloseAsync` is implemented. Would be
|
|
718
|
+
// great to rework this and make it more solid if possible.
|
|
712
719
|
} while (this.pendingOperations.size > 0)
|
|
713
720
|
this.deferCloseAllowed = false
|
|
714
721
|
|
|
@@ -975,5 +982,6 @@ export const disableDeadlockTimeoutWhenDebugging = () => {
|
|
|
975
982
|
* @internal
|
|
976
983
|
*/
|
|
977
984
|
const isDirectoryWritable = (directoryPath: string): boolean => {
|
|
985
|
+
|
|
978
986
|
return true
|
|
979
987
|
}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import * as FFI from './ffi'
|
|
6
6
|
import { CBOR } from './cbor'
|
|
7
|
+
import { mapFFIErrors } from './error'
|
|
7
8
|
|
|
8
9
|
/** Represents a unique identifier for a {@link Document}. */
|
|
9
|
-
export type DocumentIDValue = any
|
|
10
|
+
export type DocumentIDValue = any // REFACTOR: get rid of any.
|
|
10
11
|
|
|
11
12
|
/** Represents a unique identifier for a {@link Document}. */
|
|
12
13
|
export class DocumentID {
|
|
@@ -113,23 +114,26 @@ export class DocumentID {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
|
-
* Returns
|
|
117
|
+
* Returns the base64-encoded CBOR representation of this document ID.
|
|
118
|
+
*
|
|
119
|
+
* @deprecated
|
|
117
120
|
*/
|
|
118
121
|
toBase64String(): string {
|
|
119
|
-
|
|
120
|
-
return btoa(String.fromCharCode.apply(null, bytes))
|
|
122
|
+
return mapFFIErrors(() => FFI.base64encode(this['@ditto.cbor'], 'Padded'))
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
/**
|
|
124
126
|
* Returns a query compatible string representation of the receiver.
|
|
125
127
|
*
|
|
126
|
-
* The returned string can be used directly in queries that you use with
|
|
127
|
-
*
|
|
128
|
-
* this:
|
|
128
|
+
* The returned string can be used directly in queries that you use with other
|
|
129
|
+
* Ditto functions. For example you could create a query that was like this:
|
|
129
130
|
*
|
|
130
131
|
* ``` TypeScript
|
|
131
132
|
* collection.find(`_id == ${documentID.toQueryCompatibleString()}`)
|
|
132
133
|
* ```
|
|
134
|
+
*
|
|
135
|
+
* @deprecated use document IDs in queries by embedding them in the query
|
|
136
|
+
* arguments parameter.
|
|
133
137
|
*/
|
|
134
138
|
toQueryCompatibleString(): string {
|
|
135
139
|
return FFI.documentIDQueryCompatible(this['@ditto.cbor'], 'WithQuotes')
|