realm
Version:
Realm by MongoDB is an offline-first mobile database: an alternative to SQLite and key-value stores
436 lines • 19.5 kB
JavaScript
;
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
Object.defineProperty(exports, "__esModule", { value: true });
exports.SyncSession = exports.toBindingClientResetMode = exports.toBindingStopPolicy = exports.toBindingNotifyAfterClientResetWithFallback = exports.toBindingNotifyAfterClientReset = exports.toBindingNotifyBeforeClientReset = exports.toBindingErrorHandlerWithOnManual = exports.toBindingErrorHandler = exports.SessionState = exports.ConnectionState = exports.ProgressMode = exports.ProgressDirection = void 0;
const bson_1 = require("bson");
const internal_1 = require("../internal");
var ProgressDirection;
(function (ProgressDirection) {
/**
* Data going from the server to the client.
*/
ProgressDirection["Download"] = "download";
/**
* Data going from the client to the server.
*/
ProgressDirection["Upload"] = "upload";
})(ProgressDirection = exports.ProgressDirection || (exports.ProgressDirection = {}));
var ProgressMode;
(function (ProgressMode) {
ProgressMode["ReportIndefinitely"] = "reportIndefinitely";
ProgressMode["ForCurrentlyOutstandingWork"] = "forCurrentlyOutstandingWork";
})(ProgressMode = exports.ProgressMode || (exports.ProgressMode = {}));
var ConnectionState;
(function (ConnectionState) {
ConnectionState["Disconnected"] = "disconnected";
ConnectionState["Connecting"] = "connecting";
ConnectionState["Connected"] = "connected";
})(ConnectionState = exports.ConnectionState || (exports.ConnectionState = {}));
var SessionState;
(function (SessionState) {
/**
* The sync session encountered a non-recoverable error and is permanently invalid. Create a new Session to continue syncing.
*/
SessionState["Invalid"] = "invalid";
/**
* The sync session is actively communicating or attempting to communicate with Atlas App Services. A session may be considered active even if it is not currently connected. To find out if a session is online, check its connection state.
*/
SessionState["Active"] = "active";
/**
* The sync session is not attempting to communicate with Atlas App Services due to the user logging out or synchronization being paused.
*/
SessionState["Inactive"] = "inactive";
})(SessionState = exports.SessionState || (exports.SessionState = {}));
function toBindingDirection(direction) {
if (direction === ProgressDirection.Download) {
return 1 /* binding.ProgressDirection.Download */;
}
else if (direction === ProgressDirection.Upload) {
return 0 /* binding.ProgressDirection.Upload */;
}
else {
throw new Error(`Unexpected direction: ${direction}`);
}
}
function fromBindingConnectionState(state) {
if (state === 2 /* binding.SyncSessionConnectionState.Connected */) {
return ConnectionState.Connected;
}
else if (state === 1 /* binding.SyncSessionConnectionState.Connecting */) {
return ConnectionState.Connecting;
}
else if (state === 0 /* binding.SyncSessionConnectionState.Disconnected */) {
return ConnectionState.Disconnected;
}
else {
throw new Error(`Unexpected state: ${state}`);
}
}
// TODO: This mapping is an interpretation of the behavior of the legacy SDK we might want to revisit
function fromBindingSessionState(state) {
if (state === 2 /* binding.SyncSessionState.Inactive */) {
return SessionState.Inactive;
}
else {
return SessionState.Active;
}
}
/** @internal */
function toBindingErrorHandler(onError) {
return (sessionInternal, bindingError) => {
// TODO: Return some cached sync session, instead of creating a new wrapper on every error
// const session = App.Sync.getSyncSession(user, partitionValue);
const session = new SyncSession(sessionInternal);
const error = (0, internal_1.fromBindingSyncError)(bindingError);
onError(session, error);
};
}
exports.toBindingErrorHandler = toBindingErrorHandler;
/** @internal */
function toBindingErrorHandlerWithOnManual(onError, onManual) {
if (!onError && !onManual) {
throw new Error("need to set either onError or onManual or both");
}
if (onError && onManual) {
return toBindingErrorHandler((session, error) => {
if (error instanceof internal_1.ClientResetError) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onManual(session, error.config.path);
}
else {
onError(session, error);
}
});
}
if (onError) {
// onError gets all errors
return toBindingErrorHandler(onError);
}
if (onManual) {
// onManual only gets ClientResetErrors
return toBindingErrorHandler((session, error) => {
if (error instanceof internal_1.ClientResetError) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onManual(session, error.config.path);
}
});
}
}
exports.toBindingErrorHandlerWithOnManual = toBindingErrorHandlerWithOnManual;
/** @internal */
function toBindingNotifyBeforeClientReset(onBefore) {
return (internal) => {
onBefore(new internal_1.Realm(null, { internal }));
};
}
exports.toBindingNotifyBeforeClientReset = toBindingNotifyBeforeClientReset;
/** @internal */
function toBindingNotifyAfterClientReset(onAfter) {
return (internal, tsr) => {
onAfter(new internal_1.Realm(null, { internal }), new internal_1.Realm(null, { internal: internal_1.binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }));
};
}
exports.toBindingNotifyAfterClientReset = toBindingNotifyAfterClientReset;
/** @internal */
function toBindingNotifyAfterClientResetWithFallback(onAfter, onFallback) {
return (internal, tsr, didRecover) => {
if (didRecover) {
onAfter(new internal_1.Realm(null, { internal }), new internal_1.Realm(null, { internal: internal_1.binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) }));
}
else {
const realm = new internal_1.Realm(null, { internal: internal_1.binding.Helpers.consumeThreadSafeReferenceToSharedRealm(tsr) });
if (onFallback) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onFallback(realm.syncSession, realm.path);
}
else {
throw new Error("onFallback is undefined");
}
}
};
}
exports.toBindingNotifyAfterClientResetWithFallback = toBindingNotifyAfterClientResetWithFallback;
/** @internal */
function toBindingStopPolicy(policy) {
if (policy === internal_1.SessionStopPolicy.AfterUpload) {
return 2 /* binding.SyncSessionStopPolicy.AfterChangesUploaded */;
}
else if (policy === internal_1.SessionStopPolicy.Immediately) {
return 0 /* binding.SyncSessionStopPolicy.Immediately */;
}
else if (policy === internal_1.SessionStopPolicy.Never) {
return 1 /* binding.SyncSessionStopPolicy.LiveIndefinitely */;
}
else {
throw new Error(`Unexpected policy (get ${policy})`);
}
}
exports.toBindingStopPolicy = toBindingStopPolicy;
/** @internal */
function toBindingClientResetMode(resetMode) {
switch (resetMode) {
case internal_1.ClientResetMode.Manual:
return 0 /* binding.ClientResetMode.Manual */;
case internal_1.ClientResetMode.DiscardUnsyncedChanges:
return 1 /* binding.ClientResetMode.DiscardLocal */;
case internal_1.ClientResetMode.RecoverUnsyncedChanges:
return 2 /* binding.ClientResetMode.Recover */;
case internal_1.ClientResetMode.RecoverOrDiscardUnsyncedChanges:
return 3 /* binding.ClientResetMode.RecoverOrDiscard */;
}
}
exports.toBindingClientResetMode = toBindingClientResetMode;
/**
* With the current properties available through Core, it it's possible to construct an app from a user nor sync session internal.
* TODO: Refactor to pass an app instance through to all places that constructs a SyncSession.
*/
const mockApp = new Proxy({}, {
get() {
throw new Error("Using user.app of a user returned through syncSession.config is not supported");
},
});
/**
* Progress listeners are shared across instances of the SyncSession, making it possible to deregister a listener on another session
* TODO: Consider adding a check to verify that the callback is removed from the correct SyncSession (although that would break the API)
*/
const PROGRESS_LISTENERS = new internal_1.Listeners({
add(callback, weakInternal, internal, direction, mode) {
const token = internal.registerProgressNotifier((transferred, transferable) => callback(Number(transferred), Number(transferable)), toBindingDirection(direction), mode === ProgressMode.ReportIndefinitely);
return { weakInternal, token };
},
remove({ weakInternal, token }) {
weakInternal.withDeref((internal) => internal?.unregisterProgressNotifier(token));
},
});
/**
* Connection listeners are shared across instances of the SyncSession, making it possible to deregister a listener on another session
* TODO: Consider adding a check to verify that the callback is removed from the correct SyncSession (although that would break the API)
*/
const CONNECTION_LISTENERS = new internal_1.Listeners({
add(callback, weakInternal, internal) {
const token = internal.registerConnectionChangeCallback((oldState, newState) => callback(fromBindingConnectionState(newState), fromBindingConnectionState(oldState)));
return { weakInternal, token };
},
remove({ weakInternal, token }) {
weakInternal.withDeref((internal) => internal?.unregisterConnectionChangeCallback(token));
},
});
class SyncSession {
/** @internal */
weakInternal;
/** @internal */
withInternal(cb) {
return this.weakInternal.withDeref((syncSession) => {
(0, internal_1.assert)(syncSession, "This SyncSession is no longer valid");
return cb(syncSession);
});
}
/** @internal */
constructor(internal) {
this.weakInternal = internal.weaken();
}
// TODO: Return the `error_handler`
// TODO: Figure out a way to avoid passing a mocked app instance when constructing the User.
/**
* Gets the Sync-part of the configuration that the corresponding Realm was constructed with.
*/
get config() {
return this.withInternal((internal) => {
const user = new internal_1.User(internal.user, mockApp);
const { partitionValue, flxSyncRequested, customHttpHeaders, clientValidateSsl, sslTrustCertificatePath } = internal.config;
if (flxSyncRequested) {
return {
user,
flexible: true,
customHttpHeaders,
ssl: { validate: clientValidateSsl, certificatePath: sslTrustCertificatePath },
};
}
else {
return {
user,
partitionValue: bson_1.EJSON.parse(partitionValue),
customHttpHeaders,
ssl: { validate: clientValidateSsl, certificatePath: sslTrustCertificatePath },
};
}
});
}
/**
* Gets the current state of the session.
*/
get state() {
return fromBindingSessionState(this.withInternal((internal) => internal.state));
}
/**
* Gets the URL of the Realm Object Server that this session is connected to.
*/
get url() {
const url = this.withInternal((internal) => internal.fullRealmUrl);
if (url) {
return url;
}
else {
throw new Error("Unable to determine URL");
}
}
/**
* Gets the User that this session was created with.
*/
get user() {
return internal_1.User.get(this.withInternal((internal) => internal.user));
}
/**
* Gets the current state of the connection to the server. Multiple sessions might share the same underlying
* connection. In that case, any connection change is sent to all sessions.
*
* Data will only be synchronized with the server if this method returns `Connected` and `state()` returns `Active` or `Dying`.
*/
get connectionState() {
return fromBindingConnectionState(this.withInternal((internal) => internal.connectionState));
}
// TODO: Make this a getter instead of a method
/**
* Returns `true` if the session is currently active and connected to the server, `false` if not.
*/
isConnected() {
return this.withInternal((internal) => {
const { connectionState, state } = internal;
return (connectionState === 2 /* binding.SyncSessionConnectionState.Connected */ &&
(state === 0 /* binding.SyncSessionState.Active */ || state === 1 /* binding.SyncSessionState.Dying */));
});
}
/**
* Pause a sync session.
*
* This method is asynchronous so in order to know when the session has started you will need
* to add a connection notification with {@link addConnectionNotification}.
*
* This method is idempotent so it will be a no-op if the session is already paused or if multiplexing
* is enabled.
* @since 2.16.0-rc.2
*/
pause() {
this.withInternal((internal) => internal.forceClose());
}
/**
* Resumes a sync session that has been paused.
*
* This method is asynchronous so in order to know when the session has started you will need
* to add a connection notification with {@link addConnectionNotification}.
*
* This method is idempotent so it will be a no-op if the session is already started or if multiplexing
* is enabled.
* @since 2.16.0-rc.2
*/
resume() {
this.withInternal((internal) => internal.reviveIfNeeded());
}
/**
* Reconnects to Altas Device Sync.
*
* This method is asynchronous so in order to know when the session has started you will need
* to add a connection notification with {@link addConnectionNotification}.
*
* This method is idempotent so it will be a no-op if the session is already started.
* @since 12.2.0
*/
reconnect() {
this.withInternal((internal) => internal.handleReconnect());
}
/**
* Register a progress notification callback on a session object
* @param direction - The progress direction to register for.
* @param mode - The progress notification mode to use for the registration.
* Can be either:
* - `reportIndefinitely` - the registration will stay active until the callback is unregistered
* - `forCurrentlyOutstandingWork` - the registration will be active until only the currently transferable bytes are synced
* @param callback - Called with the following arguments:
* 1. `transferred`: The current number of bytes already transferred
* 2. `transferable`: The total number of transferable bytes (the number of bytes already transferred plus the number of bytes pending transfer)
* @since 1.12.0
*/
addProgressNotification(direction, mode, callback) {
this.withInternal((internal) => PROGRESS_LISTENERS.add(callback, this.weakInternal, internal, direction, mode));
}
/**
* Unregister a progress notification callback that was previously registered with {@link addProgressNotification}.
* Calling the function multiple times with the same callback is ignored.
* @param callback - A previously registered progress callback.
* @since 1.12.0
*/
removeProgressNotification(callback) {
PROGRESS_LISTENERS.remove(callback);
}
/**
* Registers a connection notification on the session object. This will be notified about changes to the
* underlying connection to the Realm Object Server.
* @param callback - Called with the following arguments:
* 1. `newState`: The new state of the connection
* 2. `oldState`: The state the connection transitioned from.
* @since 2.15.0
*/
addConnectionNotification(callback) {
this.withInternal((internal) => CONNECTION_LISTENERS.add(callback, this.weakInternal, internal));
}
/**
* Unregister a state notification callback that was previously registered with addStateNotification.
* Calling the function multiple times with the same callback is ignored.
* @param callback - A previously registered state callback.
* @since 2.15.0
*/
removeConnectionNotification(callback) {
CONNECTION_LISTENERS.remove(callback);
}
/**
* This method returns a promise that does not resolve successfully until all known remote changes have been
* downloaded and applied to the Realm or the specified timeout is hit in which case it will be rejected. If the method
* times out, the download will still continue in the background.
*
* This method cannot be called before the Realm has been opened.
* @param timeoutMs - maximum amount of time to wait in milliseconds before the promise will be rejected. If no timeout
* is specified the method will wait forever.
*/
downloadAllServerChanges(timeoutMs) {
return this.withInternal((internal) => new internal_1.TimeoutPromise(internal.waitForDownloadCompletion(), {
ms: timeoutMs,
message: `Downloading changes did not complete in ${timeoutMs} ms.`,
}));
}
/**
* This method returns a promise that does not resolve successfully until all known local changes have been uploaded
* to the server or the specified timeout is hit in which case it will be rejected. If the method times out, the upload
* will still continue in the background.
*
* This method cannot be called before the Realm has been opened.
* @param timeoutMs - Maximum amount of time to wait in milliseconds before the promise is rejected. If no timeout is specified the method will wait forever.
*/
uploadAllLocalChanges(timeoutMs) {
return this.withInternal((internal) => new internal_1.TimeoutPromise(internal.waitForUploadCompletion(), {
ms: timeoutMs,
message: `Uploading changes did not complete in ${timeoutMs} ms.`,
}));
}
/** @internal */
_simulateError(code, message, type, isFatal) {
this.withInternal((internal) => internal_1.binding.Helpers.simulateSyncError(internal, code, message, type, isFatal));
}
}
exports.SyncSession = SyncSession;
//# sourceMappingURL=SyncSession.js.map