replicache
Advanced tools
Comparing version 0.0.6 to 0.0.7
export { default } from './replicache.js'; | ||
export { TransactionClosedError } from './transaction-closed-error.js'; | ||
export { REPMHTTPInvoker } from './repm-invoker.js'; | ||
export { REPMHTTPInvoker, REPMWASMInvoker } from './repm-invoker.js'; | ||
export type { Mutator } from './replicache.js'; | ||
@@ -8,4 +8,4 @@ export type { ScanId } from './scan-id.js'; | ||
export type { DatabaseInfo } from './database-info.js'; | ||
export type { REPMInvoke } from './repm-invoker.js'; | ||
export type { REPMInvoke, Invoker } from './repm-invoker.js'; | ||
export type { ReadTransaction, WriteTransaction } from './transactions.js'; | ||
export type { ScanResult } from './scan-iterator.js'; |
@@ -9,1 +9,2 @@ "use strict"; | ||
Object.defineProperty(exports, "REPMHTTPInvoker", { enumerable: true, get: function () { return repm_invoker_js_1.REPMHTTPInvoker; } }); | ||
Object.defineProperty(exports, "REPMWASMInvoker", { enumerable: true, get: function () { return repm_invoker_js_1.REPMWASMInvoker; } }); |
import type { JSONValue } from './json.js'; | ||
import type { ScanOptions } from './scan-options.js'; | ||
import type { DatabaseInfo } from './database-info.js'; | ||
import type { REPMInvoke } from './repm-invoker.js'; | ||
import { Invoker, REPMInvoke } from './repm-invoker.js'; | ||
import { ScanResult } from './scan-iterator.js'; | ||
@@ -21,3 +21,3 @@ import type { ReadTransaction, WriteTransaction } from './transactions.js'; | ||
private readonly _name; | ||
private readonly _repmInvoke; | ||
private readonly _repmInvoker; | ||
private _closed; | ||
@@ -30,3 +30,3 @@ private _online; | ||
private readonly _subscriptions; | ||
protected _syncInterval: number | null; | ||
private _syncInterval; | ||
protected _timerId: ReturnType<typeof setTimeout> | 0; | ||
@@ -40,3 +40,3 @@ onSync: ((syncing: boolean) => void) | null; | ||
getDataLayerAuth: (() => MaybePromise<string | null | undefined>) | null | undefined; | ||
constructor({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoke, }: { | ||
constructor({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoker, syncInterval, }: { | ||
batchURL?: string; | ||
@@ -47,3 +47,4 @@ dataLayerAuth?: string; | ||
name?: string; | ||
repmInvoke: REPMInvoke; | ||
repmInvoker: Invoker; | ||
syncInterval?: number | null; | ||
}); | ||
@@ -63,2 +64,3 @@ /** | ||
}): Promise<void>; | ||
get isWASM(): boolean; | ||
get online(): boolean; | ||
@@ -73,2 +75,3 @@ get closed(): boolean; | ||
private _scheduleSync; | ||
private _clearTimer; | ||
close(): Promise<void>; | ||
@@ -112,3 +115,3 @@ private _getRoot; | ||
* If an error occurs in the `body` the `onError` function is called if | ||
* present. | ||
* present. Otherwise, the error is thrown. | ||
*/ | ||
@@ -161,3 +164,3 @@ subscribe<R, E>(body: (tx: ReadTransaction) => Promise<R>, { onData, onError, onDone, }: { | ||
export declare class ReplicacheTest extends Replicache { | ||
static new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoke, }: { | ||
static new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoker, }: { | ||
diffServerURL: string; | ||
@@ -168,6 +171,4 @@ batchURL?: string; | ||
name?: string; | ||
repmInvoke: REPMInvoke; | ||
repmInvoker: Invoker; | ||
}): Promise<ReplicacheTest>; | ||
/** @override */ | ||
protected _syncInterval: number | null; | ||
beginSync(): Promise<BeginSyncResult>; | ||
@@ -174,0 +175,0 @@ maybeEndSync(beginSyncResult: BeginSyncResult): Promise<void>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ReplicacheTest = exports.httpStatusUnauthorized = void 0; | ||
const repm_invoker_js_1 = require("./repm-invoker.js"); | ||
const transactions_js_1 = require("./transactions.js"); | ||
@@ -8,3 +9,3 @@ const scan_iterator_js_1 = require("./scan-iterator.js"); | ||
class Replicache { | ||
constructor({ batchURL = '', dataLayerAuth = '', diffServerAuth = '', diffServerURL, name = 'default', repmInvoke, }) { | ||
constructor({ batchURL = '', dataLayerAuth = '', diffServerAuth = '', diffServerURL, name = 'default', repmInvoker = new repm_invoker_js_1.REPMWASMInvoker(), syncInterval = 60000, }) { | ||
this._closed = false; | ||
@@ -17,3 +18,2 @@ this._online = true; | ||
this._subscriptions = new Set(); | ||
this._syncInterval = 60000; | ||
// NodeJS has a non standard setTimeout function :'( | ||
@@ -30,3 +30,3 @@ this._timerId = 0; | ||
await this._opened; | ||
return await this._repmInvoke(this._name, rpc, args); | ||
return await this._repmInvoker.invoke(this._name, rpc, args); | ||
}; | ||
@@ -38,3 +38,4 @@ this._batchURL = batchURL; | ||
this._name = name; | ||
this._repmInvoke = repmInvoke; | ||
this._repmInvoker = repmInvoker; | ||
this._syncInterval = syncInterval; | ||
this._open(); | ||
@@ -50,3 +51,3 @@ } | ||
async _open() { | ||
this._opened = this._repmInvoke(this._name, 'open'); | ||
this._opened = this._repmInvoker.invoke(this._name, 'open'); | ||
this._root = this._getRoot(); | ||
@@ -64,2 +65,5 @@ await this._root; | ||
} | ||
get isWASM() { | ||
return this._repmInvoker.isWASM || false; | ||
} | ||
get online() { | ||
@@ -79,6 +83,3 @@ return this._online; | ||
set syncInterval(duration) { | ||
if (this._timerId !== 0) { | ||
clearTimeout(this._timerId); | ||
this._timerId = 0; | ||
} | ||
this._clearTimer(); | ||
this._syncInterval = duration; | ||
@@ -92,2 +93,8 @@ this._scheduleSync(); | ||
} | ||
_clearTimer() { | ||
if (this._timerId !== 0) { | ||
clearTimeout(this._timerId); | ||
this._timerId = 0; | ||
} | ||
} | ||
async close() { | ||
@@ -97,3 +104,3 @@ var _a; | ||
const p = this._invoke('close'); | ||
// Clear timer | ||
this._clearTimer(); | ||
// Clear subscriptions | ||
@@ -135,7 +142,7 @@ for (const subscription of this._subscriptions) { | ||
let tx; | ||
return new scan_iterator_js_1.ScanResult(prefix, start, this._invoke, async () => { | ||
return new scan_iterator_js_1.ScanResult(this.isWASM, prefix, start, this._invoke, async () => { | ||
if (tx) { | ||
return tx; | ||
} | ||
tx = new transactions_js_1.ReadTransactionImpl(this._invoke); | ||
tx = new transactions_js_1.ReadTransactionImpl(this.isWASM, this._invoke); | ||
await tx.open({}); | ||
@@ -148,3 +155,6 @@ return tx; | ||
const beginSyncResult = await this._beginSync(); | ||
if (beginSyncResult.syncHead !== '00000000000000000000000000000000') { | ||
// TODO(repc-switchover) | ||
// replicache-client sends all zeros for null sync, | ||
// repc sends empty string. | ||
if (beginSyncResult.syncHead.replace(/0/g, '') !== '') { | ||
await this._maybeEndSync(beginSyncResult); | ||
@@ -174,2 +184,3 @@ } | ||
let reauth = false; | ||
// TODO:(repc-switchover): checkStatus only used by replicache-client. | ||
function checkStatus(data, serverName) { | ||
@@ -184,3 +195,3 @@ const { httpStatusCode, errorMessage } = data; | ||
} | ||
const { batchPushInfo } = syncInfo; | ||
const { batchPushInfo } = { ...syncInfo }; | ||
if (batchPushInfo) { | ||
@@ -195,3 +206,6 @@ checkStatus(batchPushInfo, 'batch'); | ||
} | ||
checkStatus(syncInfo.clientViewInfo, 'client view'); | ||
const { clientViewInfo } = { ...syncInfo }; | ||
if (clientViewInfo) { | ||
checkStatus(syncInfo.clientViewInfo, 'client view'); | ||
} | ||
if (reauth && this.getDataLayerAuth) { | ||
@@ -258,6 +272,3 @@ const dataLayerAuth = await this.getDataLayerAuth(); | ||
} | ||
if (this._timerId !== 0) { | ||
clearTimeout(this._timerId); | ||
this._timerId = 0; | ||
} | ||
this._clearTimer(); | ||
this._fireOnSync(true); | ||
@@ -312,3 +323,3 @@ try { | ||
* If an error occurs in the `body` the `onError` function is called if | ||
* present. | ||
* present. Otherwise, the error is thrown. | ||
*/ | ||
@@ -319,3 +330,2 @@ subscribe(body, { onData, onError, onDone, }) { | ||
(async () => { | ||
var _a; | ||
try { | ||
@@ -326,3 +336,8 @@ const res = await this.query(s.body); | ||
catch (ex) { | ||
(_a = s.onError) === null || _a === void 0 ? void 0 : _a.call(s, ex); | ||
if (s.onError) { | ||
s.onError(ex); | ||
} | ||
else { | ||
throw ex; | ||
} | ||
} | ||
@@ -340,3 +355,3 @@ })(); | ||
async query(body) { | ||
const tx = new transactions_js_1.ReadTransactionImpl(this._invoke); | ||
const tx = new transactions_js_1.ReadTransactionImpl(this.isWASM, this._invoke); | ||
await tx.open({}); | ||
@@ -393,3 +408,3 @@ try { | ||
let result; | ||
const tx = new transactions_js_1.WriteTransactionImpl(this._invoke); | ||
const tx = new transactions_js_1.WriteTransactionImpl(this.isWASM, this._invoke); | ||
await tx.open(actualInvokeArgs); | ||
@@ -420,8 +435,3 @@ try { | ||
class ReplicacheTest extends Replicache { | ||
constructor() { | ||
super(...arguments); | ||
/** @override */ | ||
this._syncInterval = null; | ||
} | ||
static async new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name = '', repmInvoke, }) { | ||
static async new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name = '', repmInvoker, }) { | ||
const rep = new ReplicacheTest({ | ||
@@ -433,6 +443,6 @@ batchURL, | ||
name, | ||
repmInvoke, | ||
repmInvoker, | ||
syncInterval: null, | ||
}); | ||
await rep._opened; | ||
// await this._root; | ||
return rep; | ||
@@ -439,0 +449,0 @@ } |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -13,3 +32,3 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
const replicache_js_1 = require("./replicache.js"); | ||
const mod_js_1 = require("./mod.js"); | ||
const mod_js_1 = __importStar(require("./mod.js")); | ||
const scan_iterator_js_1 = require("./scan-iterator.js"); | ||
@@ -131,3 +150,5 @@ let rep = null; | ||
name, | ||
repmInvoke: invoke, | ||
repmInvoker: { | ||
invoke, | ||
}, | ||
}); | ||
@@ -592,1 +613,87 @@ } | ||
}); | ||
test('syncInterval in constructor', async () => { | ||
await useReplay('syncInterval in constructor'); | ||
const rep = new mod_js_1.default({ | ||
syncInterval: 12.34, | ||
repmInvoker: { invoke }, | ||
diffServerURL: 'xxx', | ||
}); | ||
expect(rep.syncInterval).toBe(12.34); | ||
await rep.close(); | ||
}); | ||
test('closeTransaction after rep.scan', async () => { | ||
await useReplay('closeTransaction after rep.scan'); | ||
invoke = jest.fn(invoke); | ||
rep = await replicacheForTesting('test5'); | ||
const add = rep.register('add-data', addData); | ||
await add({ | ||
'a/0': 0, | ||
'a/1': 1, | ||
}); | ||
const mockInvoke = invoke; | ||
mockInvoke.mockClear(); | ||
function expectCalls(log) { | ||
expect(log).toEqual(log); | ||
const rpcs = mockInvoke.mock.calls.map(([, rpc]) => rpc); | ||
expect(rpcs).toEqual(['openTransaction', 'scan', 'closeTransaction']); | ||
} | ||
const it = rep.scan(); | ||
const log = []; | ||
for await (const v of it) { | ||
log.push(v); | ||
} | ||
expectCalls([0, 1]); | ||
// One more time with return in loop... | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
await (async () => { | ||
if (!rep) { | ||
fail(); | ||
} | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
return; | ||
} | ||
})(); | ||
expectCalls([0]); | ||
// ... and with a break. | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
{ | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
break; | ||
} | ||
} | ||
expectCalls([0]); | ||
// ... and with a throw. | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
await expect((async () => { | ||
if (!rep) { | ||
fail(); | ||
} | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
throw 'hi!'; | ||
} | ||
})()).rejects.toBe('hi!'); | ||
expectCalls([0]); | ||
// ... and with a throw. | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
await expect((async () => { | ||
if (!rep) { | ||
fail(); | ||
} | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
throw 'hi!'; | ||
} | ||
})()).rejects.toBe('hi!'); | ||
expectCalls([0]); | ||
}); |
@@ -5,2 +5,6 @@ import type { JSONValue, ToJSON } from './json.js'; | ||
import type { DatabaseInfo } from './database-info.js'; | ||
export declare type Invoker = { | ||
readonly invoke: REPMInvoke; | ||
readonly isWASM?: boolean; | ||
}; | ||
export interface Invoke { | ||
@@ -21,2 +25,9 @@ <Rpc extends keyof InvokeMapNoArgs>(rpc: Rpc): Promise<InvokeMapNoArgs[Rpc]>; | ||
} | ||
export declare class REPMWASMInvoker { | ||
private readonly _inited; | ||
private _dispatch?; | ||
readonly isWASM = true; | ||
constructor(wasm_module?: any); | ||
invoke: REPMInvoke; | ||
} | ||
declare type GetRequest = TransactionRequest & { | ||
@@ -39,5 +50,7 @@ key: string; | ||
export declare type ScanRequest = TransactionRequest & ScanOptions & { | ||
limit?: number; | ||
opts?: ScanOptions; | ||
}; | ||
export declare type ScanResponse = ScanItem[]; | ||
export declare type ScanResponse = ScanItem[] | { | ||
items: ScanItem[]; | ||
}; | ||
declare type PutRequest = TransactionRequest & { | ||
@@ -44,0 +57,0 @@ key: string; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.REPMHTTPInvoker = void 0; | ||
exports.REPMWASMInvoker = exports.REPMHTTPInvoker = void 0; | ||
class REPMHTTPInvoker { | ||
@@ -23,1 +42,23 @@ constructor(url) { | ||
exports.REPMHTTPInvoker = REPMHTTPInvoker; | ||
class REPMWASMInvoker { | ||
constructor(wasm_module) { | ||
this.isWASM = true; | ||
this.invoke = async (dbName, rpc, args = {}) => { | ||
console.debug(">", dbName, rpc, args); | ||
await this._inited; | ||
const json = await this._dispatch(dbName, rpc, JSON.stringify(args)); // eslint-disable-line @typescript-eslint/no-non-null-assertion | ||
const ret = json == '' ? null : JSON.parse(json); | ||
console.debug("<", dbName, rpc, ret); | ||
return ret; | ||
}; | ||
this._inited = (async () => { | ||
// TODO: Have to import dynamically to hide this from Jest. | ||
// Jest cannot parse the es6 behind this import, I don't know why. | ||
// TODO: We need to have some way to switch between debug and release. | ||
const { default: init, dispatch } = await Promise.resolve().then(() => __importStar(require('./wasm/debug/replicache_client.js'))); | ||
this._dispatch = dispatch; | ||
return init(wasm_module); | ||
})(); | ||
} | ||
} | ||
exports.REPMWASMInvoker = REPMWASMInvoker; |
@@ -17,2 +17,3 @@ import type { ScanBound } from './scan-bound.js'; | ||
export declare class ScanIterator<V> implements AsyncIterableIterator<V> { | ||
private readonly _isWASM; | ||
private readonly _scanItems; | ||
@@ -29,3 +30,3 @@ private _current; | ||
private readonly _invoke; | ||
constructor(kind: ScanIterableKind, prefix: string, start: ScanBound | undefined, invoke: Invoke, getTransaction: () => Promise<IdCloser> | IdCloser, shouldCloseTranscation: boolean); | ||
constructor(isWASM: boolean, kind: ScanIterableKind, prefix: string, start: ScanBound | undefined, invoke: Invoke, getTransaction: () => Promise<IdCloser> | IdCloser, shouldCloseTranscation: boolean); | ||
[Symbol.asyncIterator](): AsyncIterableIterator<V>; | ||
@@ -38,4 +39,5 @@ private _ensureTransaction; | ||
export declare class ScanResult implements AsyncIterable<JSONValue> { | ||
private readonly _isWASM; | ||
private readonly _args; | ||
constructor(...args: [string, ScanBound | undefined, Invoke, () => Promise<IdCloser> | IdCloser, boolean]); | ||
constructor(isWASM: boolean, ...args: [string, ScanBound | undefined, Invoke, () => Promise<IdCloser> | IdCloser, boolean]); | ||
[Symbol.asyncIterator](): AsyncIterableIterator<JSONValue>; | ||
@@ -42,0 +44,0 @@ values(): AsyncIterableIterator<JSONValue>; |
@@ -5,3 +5,4 @@ "use strict"; | ||
const transactions_js_1 = require("./transactions.js"); | ||
exports.scanPageSize = 100; | ||
const defaultScanSize = 500; | ||
exports.scanPageSize = defaultScanSize; | ||
function setScanPageSizeForTesting(n) { | ||
@@ -12,3 +13,3 @@ exports.scanPageSize = n; | ||
function restoreScanPageSizeForTesting() { | ||
exports.scanPageSize = 100; | ||
exports.scanPageSize = defaultScanSize; | ||
} | ||
@@ -20,3 +21,3 @@ exports.restoreScanPageSizeForTesting = restoreScanPageSizeForTesting; | ||
class ScanIterator { | ||
constructor(kind, prefix, start, invoke, getTransaction, shouldCloseTranscation) { | ||
constructor(isWASM, kind, prefix, start, invoke, getTransaction, shouldCloseTranscation) { | ||
this._scanItems = []; | ||
@@ -27,2 +28,3 @@ this._current = 0; | ||
this._transaction = undefined; | ||
this._isWASM = isWASM; | ||
this._kind = kind; | ||
@@ -55,3 +57,3 @@ this._prefix = prefix; | ||
if (!this._moreItemsToLoad) { | ||
return { done: true, value: undefined }; | ||
return this.return(); | ||
} | ||
@@ -102,11 +104,23 @@ this._loadPromise = this._load(); | ||
} | ||
const scanItems = await this._invoke('scan', { | ||
transactionId: this._transaction.id, | ||
const opts = { | ||
prefix: this._prefix, | ||
start, | ||
limit: exports.scanPageSize, | ||
}); | ||
}; | ||
const args = { | ||
transactionId: this._transaction.id, | ||
...(this._isWASM ? { opts } : opts), | ||
}; | ||
const response = await this._invoke('scan', args); | ||
// TODO(repc-switchover): only the !array path is needed for repc. | ||
const scanItems = Array.isArray(response) ? response : response.items; | ||
if (scanItems.length !== exports.scanPageSize) { | ||
this._moreItemsToLoad = false; | ||
} | ||
if (this._isWASM) { | ||
for (const item of scanItems) { | ||
// Temporarily circument the readonly-ness of item.value to parse. | ||
item.value = JSON.parse(item.value); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
} | ||
} | ||
this._scanItems.push(...scanItems); | ||
@@ -117,3 +131,4 @@ } | ||
class ScanResult { | ||
constructor(...args) { | ||
constructor(isWASM, ...args) { | ||
this._isWASM = isWASM; | ||
this._args = args; | ||
@@ -125,14 +140,14 @@ } | ||
values() { | ||
return this._newIterator('value'); | ||
return this._newIterator(this._isWASM, 'value'); | ||
} | ||
keys() { | ||
return this._newIterator('key'); | ||
return this._newIterator(this._isWASM, 'key'); | ||
} | ||
entries() { | ||
return this._newIterator('entry'); | ||
return this._newIterator(this._isWASM, 'entry'); | ||
} | ||
_newIterator(kind) { | ||
return new ScanIterator(kind, ...this._args); | ||
_newIterator(isWASM, kind) { | ||
return new ScanIterator(isWASM, kind, ...this._args); | ||
} | ||
} | ||
exports.ScanResult = ScanResult; |
@@ -5,2 +5,3 @@ import type { ScanBound } from './scan-bound.js'; | ||
start?: ScanBound; | ||
limit?: number; | ||
}; |
@@ -34,5 +34,6 @@ import type { JSONValue, ToJSON } from './json.js'; | ||
private _transactionId; | ||
protected readonly _isWASM: boolean; | ||
protected readonly _invoke: Invoke; | ||
protected _closed: boolean; | ||
constructor(invoke: Invoke); | ||
constructor(isWASM: boolean, invoke: Invoke); | ||
get(key: string): Promise<JSONValue | undefined>; | ||
@@ -39,0 +40,0 @@ has(key: string): Promise<boolean>; |
@@ -13,5 +13,6 @@ "use strict"; | ||
class ReadTransactionImpl { | ||
constructor(invoke) { | ||
constructor(isWASM, invoke) { | ||
this._transactionId = -1; | ||
this._closed = false; | ||
this._isWASM = isWASM; | ||
this._invoke = invoke; | ||
@@ -28,3 +29,3 @@ } | ||
} | ||
return result.value; | ||
return this._isWASM ? JSON.parse(result.value) : result.value; | ||
} | ||
@@ -40,3 +41,3 @@ async has(key) { | ||
scan({ prefix = '', start } = {}) { | ||
return new scan_iterator_js_1.ScanResult(prefix, start, this._invoke, () => this, false); | ||
return new scan_iterator_js_1.ScanResult(this._isWASM, prefix, start, this._invoke, () => this, false); | ||
} | ||
@@ -72,3 +73,3 @@ get id() { | ||
key, | ||
value, | ||
value: this._isWASM ? JSON.stringify(value) : value, | ||
}); | ||
@@ -75,0 +76,0 @@ } |
export { default } from './replicache.js'; | ||
export { TransactionClosedError } from './transaction-closed-error.js'; | ||
export { REPMHTTPInvoker } from './repm-invoker.js'; | ||
export { REPMHTTPInvoker, REPMWASMInvoker } from './repm-invoker.js'; | ||
export type { Mutator } from './replicache.js'; | ||
@@ -8,4 +8,4 @@ export type { ScanId } from './scan-id.js'; | ||
export type { DatabaseInfo } from './database-info.js'; | ||
export type { REPMInvoke } from './repm-invoker.js'; | ||
export type { REPMInvoke, Invoker } from './repm-invoker.js'; | ||
export type { ReadTransaction, WriteTransaction } from './transactions.js'; | ||
export type { ScanResult } from './scan-iterator.js'; |
export { default } from './replicache.js'; | ||
export { TransactionClosedError } from './transaction-closed-error.js'; | ||
export { REPMHTTPInvoker } from './repm-invoker.js'; | ||
export { REPMHTTPInvoker, REPMWASMInvoker } from './repm-invoker.js'; |
import type { JSONValue } from './json.js'; | ||
import type { ScanOptions } from './scan-options.js'; | ||
import type { DatabaseInfo } from './database-info.js'; | ||
import type { REPMInvoke } from './repm-invoker.js'; | ||
import { Invoker, REPMInvoke } from './repm-invoker.js'; | ||
import { ScanResult } from './scan-iterator.js'; | ||
@@ -21,3 +21,3 @@ import type { ReadTransaction, WriteTransaction } from './transactions.js'; | ||
private readonly _name; | ||
private readonly _repmInvoke; | ||
private readonly _repmInvoker; | ||
private _closed; | ||
@@ -30,3 +30,3 @@ private _online; | ||
private readonly _subscriptions; | ||
protected _syncInterval: number | null; | ||
private _syncInterval; | ||
protected _timerId: ReturnType<typeof setTimeout> | 0; | ||
@@ -40,3 +40,3 @@ onSync: ((syncing: boolean) => void) | null; | ||
getDataLayerAuth: (() => MaybePromise<string | null | undefined>) | null | undefined; | ||
constructor({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoke, }: { | ||
constructor({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoker, syncInterval, }: { | ||
batchURL?: string; | ||
@@ -47,3 +47,4 @@ dataLayerAuth?: string; | ||
name?: string; | ||
repmInvoke: REPMInvoke; | ||
repmInvoker: Invoker; | ||
syncInterval?: number | null; | ||
}); | ||
@@ -63,2 +64,3 @@ /** | ||
}): Promise<void>; | ||
get isWASM(): boolean; | ||
get online(): boolean; | ||
@@ -73,2 +75,3 @@ get closed(): boolean; | ||
private _scheduleSync; | ||
private _clearTimer; | ||
close(): Promise<void>; | ||
@@ -112,3 +115,3 @@ private _getRoot; | ||
* If an error occurs in the `body` the `onError` function is called if | ||
* present. | ||
* present. Otherwise, the error is thrown. | ||
*/ | ||
@@ -161,3 +164,3 @@ subscribe<R, E>(body: (tx: ReadTransaction) => Promise<R>, { onData, onError, onDone, }: { | ||
export declare class ReplicacheTest extends Replicache { | ||
static new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoke, }: { | ||
static new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name, repmInvoker, }: { | ||
diffServerURL: string; | ||
@@ -168,6 +171,4 @@ batchURL?: string; | ||
name?: string; | ||
repmInvoke: REPMInvoke; | ||
repmInvoker: Invoker; | ||
}): Promise<ReplicacheTest>; | ||
/** @override */ | ||
protected _syncInterval: number | null; | ||
beginSync(): Promise<BeginSyncResult>; | ||
@@ -174,0 +175,0 @@ maybeEndSync(beginSyncResult: BeginSyncResult): Promise<void>; |
@@ -0,1 +1,2 @@ | ||
import { REPMWASMInvoker, } from './repm-invoker.js'; | ||
import { ReadTransactionImpl, WriteTransactionImpl } from './transactions.js'; | ||
@@ -5,3 +6,3 @@ import { ScanResult } from './scan-iterator.js'; | ||
export default class Replicache { | ||
constructor({ batchURL = '', dataLayerAuth = '', diffServerAuth = '', diffServerURL, name = 'default', repmInvoke, }) { | ||
constructor({ batchURL = '', dataLayerAuth = '', diffServerAuth = '', diffServerURL, name = 'default', repmInvoker = new REPMWASMInvoker(), syncInterval = 60000, }) { | ||
this._closed = false; | ||
@@ -14,3 +15,2 @@ this._online = true; | ||
this._subscriptions = new Set(); | ||
this._syncInterval = 60000; | ||
// NodeJS has a non standard setTimeout function :'( | ||
@@ -27,3 +27,3 @@ this._timerId = 0; | ||
await this._opened; | ||
return await this._repmInvoke(this._name, rpc, args); | ||
return await this._repmInvoker.invoke(this._name, rpc, args); | ||
}; | ||
@@ -35,3 +35,4 @@ this._batchURL = batchURL; | ||
this._name = name; | ||
this._repmInvoke = repmInvoke; | ||
this._repmInvoker = repmInvoker; | ||
this._syncInterval = syncInterval; | ||
this._open(); | ||
@@ -47,3 +48,3 @@ } | ||
async _open() { | ||
this._opened = this._repmInvoke(this._name, 'open'); | ||
this._opened = this._repmInvoker.invoke(this._name, 'open'); | ||
this._root = this._getRoot(); | ||
@@ -61,2 +62,5 @@ await this._root; | ||
} | ||
get isWASM() { | ||
return this._repmInvoker.isWASM || false; | ||
} | ||
get online() { | ||
@@ -76,6 +80,3 @@ return this._online; | ||
set syncInterval(duration) { | ||
if (this._timerId !== 0) { | ||
clearTimeout(this._timerId); | ||
this._timerId = 0; | ||
} | ||
this._clearTimer(); | ||
this._syncInterval = duration; | ||
@@ -89,2 +90,8 @@ this._scheduleSync(); | ||
} | ||
_clearTimer() { | ||
if (this._timerId !== 0) { | ||
clearTimeout(this._timerId); | ||
this._timerId = 0; | ||
} | ||
} | ||
async close() { | ||
@@ -94,3 +101,3 @@ var _a; | ||
const p = this._invoke('close'); | ||
// Clear timer | ||
this._clearTimer(); | ||
// Clear subscriptions | ||
@@ -132,7 +139,7 @@ for (const subscription of this._subscriptions) { | ||
let tx; | ||
return new ScanResult(prefix, start, this._invoke, async () => { | ||
return new ScanResult(this.isWASM, prefix, start, this._invoke, async () => { | ||
if (tx) { | ||
return tx; | ||
} | ||
tx = new ReadTransactionImpl(this._invoke); | ||
tx = new ReadTransactionImpl(this.isWASM, this._invoke); | ||
await tx.open({}); | ||
@@ -145,3 +152,6 @@ return tx; | ||
const beginSyncResult = await this._beginSync(); | ||
if (beginSyncResult.syncHead !== '00000000000000000000000000000000') { | ||
// TODO(repc-switchover) | ||
// replicache-client sends all zeros for null sync, | ||
// repc sends empty string. | ||
if (beginSyncResult.syncHead.replace(/0/g, '') !== '') { | ||
await this._maybeEndSync(beginSyncResult); | ||
@@ -171,2 +181,3 @@ } | ||
let reauth = false; | ||
// TODO:(repc-switchover): checkStatus only used by replicache-client. | ||
function checkStatus(data, serverName) { | ||
@@ -181,3 +192,3 @@ const { httpStatusCode, errorMessage } = data; | ||
} | ||
const { batchPushInfo } = syncInfo; | ||
const { batchPushInfo } = { ...syncInfo }; | ||
if (batchPushInfo) { | ||
@@ -192,3 +203,6 @@ checkStatus(batchPushInfo, 'batch'); | ||
} | ||
checkStatus(syncInfo.clientViewInfo, 'client view'); | ||
const { clientViewInfo } = { ...syncInfo }; | ||
if (clientViewInfo) { | ||
checkStatus(syncInfo.clientViewInfo, 'client view'); | ||
} | ||
if (reauth && this.getDataLayerAuth) { | ||
@@ -255,6 +269,3 @@ const dataLayerAuth = await this.getDataLayerAuth(); | ||
} | ||
if (this._timerId !== 0) { | ||
clearTimeout(this._timerId); | ||
this._timerId = 0; | ||
} | ||
this._clearTimer(); | ||
this._fireOnSync(true); | ||
@@ -309,3 +320,3 @@ try { | ||
* If an error occurs in the `body` the `onError` function is called if | ||
* present. | ||
* present. Otherwise, the error is thrown. | ||
*/ | ||
@@ -316,3 +327,2 @@ subscribe(body, { onData, onError, onDone, }) { | ||
(async () => { | ||
var _a; | ||
try { | ||
@@ -323,3 +333,8 @@ const res = await this.query(s.body); | ||
catch (ex) { | ||
(_a = s.onError) === null || _a === void 0 ? void 0 : _a.call(s, ex); | ||
if (s.onError) { | ||
s.onError(ex); | ||
} | ||
else { | ||
throw ex; | ||
} | ||
} | ||
@@ -337,3 +352,3 @@ })(); | ||
async query(body) { | ||
const tx = new ReadTransactionImpl(this._invoke); | ||
const tx = new ReadTransactionImpl(this.isWASM, this._invoke); | ||
await tx.open({}); | ||
@@ -390,3 +405,3 @@ try { | ||
let result; | ||
const tx = new WriteTransactionImpl(this._invoke); | ||
const tx = new WriteTransactionImpl(this.isWASM, this._invoke); | ||
await tx.open(actualInvokeArgs); | ||
@@ -416,8 +431,3 @@ try { | ||
export class ReplicacheTest extends Replicache { | ||
constructor() { | ||
super(...arguments); | ||
/** @override */ | ||
this._syncInterval = null; | ||
} | ||
static async new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name = '', repmInvoke, }) { | ||
static async new({ batchURL, dataLayerAuth, diffServerAuth, diffServerURL, name = '', repmInvoker, }) { | ||
const rep = new ReplicacheTest({ | ||
@@ -429,6 +439,6 @@ batchURL, | ||
name, | ||
repmInvoke, | ||
repmInvoker, | ||
syncInterval: null, | ||
}); | ||
await rep._opened; | ||
// await this._root; | ||
return rep; | ||
@@ -435,0 +445,0 @@ } |
@@ -8,3 +8,3 @@ var _a; | ||
import { ReplicacheTest, httpStatusUnauthorized } from './replicache.js'; | ||
import { REPMHTTPInvoker, TransactionClosedError } from './mod.js'; | ||
import Replicache, { REPMHTTPInvoker, TransactionClosedError, } from './mod.js'; | ||
import { restoreScanPageSizeForTesting, setScanPageSizeForTesting, } from './scan-iterator.js'; | ||
@@ -126,3 +126,5 @@ let rep = null; | ||
name, | ||
repmInvoke: invoke, | ||
repmInvoker: { | ||
invoke, | ||
}, | ||
}); | ||
@@ -587,1 +589,87 @@ } | ||
}); | ||
test('syncInterval in constructor', async () => { | ||
await useReplay('syncInterval in constructor'); | ||
const rep = new Replicache({ | ||
syncInterval: 12.34, | ||
repmInvoker: { invoke }, | ||
diffServerURL: 'xxx', | ||
}); | ||
expect(rep.syncInterval).toBe(12.34); | ||
await rep.close(); | ||
}); | ||
test('closeTransaction after rep.scan', async () => { | ||
await useReplay('closeTransaction after rep.scan'); | ||
invoke = jest.fn(invoke); | ||
rep = await replicacheForTesting('test5'); | ||
const add = rep.register('add-data', addData); | ||
await add({ | ||
'a/0': 0, | ||
'a/1': 1, | ||
}); | ||
const mockInvoke = invoke; | ||
mockInvoke.mockClear(); | ||
function expectCalls(log) { | ||
expect(log).toEqual(log); | ||
const rpcs = mockInvoke.mock.calls.map(([, rpc]) => rpc); | ||
expect(rpcs).toEqual(['openTransaction', 'scan', 'closeTransaction']); | ||
} | ||
const it = rep.scan(); | ||
const log = []; | ||
for await (const v of it) { | ||
log.push(v); | ||
} | ||
expectCalls([0, 1]); | ||
// One more time with return in loop... | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
await (async () => { | ||
if (!rep) { | ||
fail(); | ||
} | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
return; | ||
} | ||
})(); | ||
expectCalls([0]); | ||
// ... and with a break. | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
{ | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
break; | ||
} | ||
} | ||
expectCalls([0]); | ||
// ... and with a throw. | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
await expect((async () => { | ||
if (!rep) { | ||
fail(); | ||
} | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
throw 'hi!'; | ||
} | ||
})()).rejects.toBe('hi!'); | ||
expectCalls([0]); | ||
// ... and with a throw. | ||
log.length = 0; | ||
mockInvoke.mockClear(); | ||
await expect((async () => { | ||
if (!rep) { | ||
fail(); | ||
} | ||
const it = rep.scan(); | ||
for await (const v of it) { | ||
log.push(v); | ||
throw 'hi!'; | ||
} | ||
})()).rejects.toBe('hi!'); | ||
expectCalls([0]); | ||
}); |
@@ -5,2 +5,6 @@ import type { JSONValue, ToJSON } from './json.js'; | ||
import type { DatabaseInfo } from './database-info.js'; | ||
export declare type Invoker = { | ||
readonly invoke: REPMInvoke; | ||
readonly isWASM?: boolean; | ||
}; | ||
export interface Invoke { | ||
@@ -21,2 +25,9 @@ <Rpc extends keyof InvokeMapNoArgs>(rpc: Rpc): Promise<InvokeMapNoArgs[Rpc]>; | ||
} | ||
export declare class REPMWASMInvoker { | ||
private readonly _inited; | ||
private _dispatch?; | ||
readonly isWASM = true; | ||
constructor(wasm_module?: any); | ||
invoke: REPMInvoke; | ||
} | ||
declare type GetRequest = TransactionRequest & { | ||
@@ -39,5 +50,7 @@ key: string; | ||
export declare type ScanRequest = TransactionRequest & ScanOptions & { | ||
limit?: number; | ||
opts?: ScanOptions; | ||
}; | ||
export declare type ScanResponse = ScanItem[]; | ||
export declare type ScanResponse = ScanItem[] | { | ||
items: ScanItem[]; | ||
}; | ||
declare type PutRequest = TransactionRequest & { | ||
@@ -44,0 +57,0 @@ key: string; |
@@ -19,1 +19,22 @@ export class REPMHTTPInvoker { | ||
} | ||
export class REPMWASMInvoker { | ||
constructor(wasm_module) { | ||
this.isWASM = true; | ||
this.invoke = async (dbName, rpc, args = {}) => { | ||
console.debug(">", dbName, rpc, args); | ||
await this._inited; | ||
const json = await this._dispatch(dbName, rpc, JSON.stringify(args)); // eslint-disable-line @typescript-eslint/no-non-null-assertion | ||
const ret = json == '' ? null : JSON.parse(json); | ||
console.debug("<", dbName, rpc, ret); | ||
return ret; | ||
}; | ||
this._inited = (async () => { | ||
// TODO: Have to import dynamically to hide this from Jest. | ||
// Jest cannot parse the es6 behind this import, I don't know why. | ||
// TODO: We need to have some way to switch between debug and release. | ||
const { default: init, dispatch } = await import('./wasm/debug/replicache_client.js'); | ||
this._dispatch = dispatch; | ||
return init(wasm_module); | ||
})(); | ||
} | ||
} |
@@ -17,2 +17,3 @@ import type { ScanBound } from './scan-bound.js'; | ||
export declare class ScanIterator<V> implements AsyncIterableIterator<V> { | ||
private readonly _isWASM; | ||
private readonly _scanItems; | ||
@@ -29,3 +30,3 @@ private _current; | ||
private readonly _invoke; | ||
constructor(kind: ScanIterableKind, prefix: string, start: ScanBound | undefined, invoke: Invoke, getTransaction: () => Promise<IdCloser> | IdCloser, shouldCloseTranscation: boolean); | ||
constructor(isWASM: boolean, kind: ScanIterableKind, prefix: string, start: ScanBound | undefined, invoke: Invoke, getTransaction: () => Promise<IdCloser> | IdCloser, shouldCloseTranscation: boolean); | ||
[Symbol.asyncIterator](): AsyncIterableIterator<V>; | ||
@@ -38,4 +39,5 @@ private _ensureTransaction; | ||
export declare class ScanResult implements AsyncIterable<JSONValue> { | ||
private readonly _isWASM; | ||
private readonly _args; | ||
constructor(...args: [string, ScanBound | undefined, Invoke, () => Promise<IdCloser> | IdCloser, boolean]); | ||
constructor(isWASM: boolean, ...args: [string, ScanBound | undefined, Invoke, () => Promise<IdCloser> | IdCloser, boolean]); | ||
[Symbol.asyncIterator](): AsyncIterableIterator<JSONValue>; | ||
@@ -42,0 +44,0 @@ values(): AsyncIterableIterator<JSONValue>; |
import { throwIfClosed } from './transactions.js'; | ||
export let scanPageSize = 100; | ||
const defaultScanSize = 500; | ||
export let scanPageSize = defaultScanSize; | ||
export function setScanPageSizeForTesting(n) { | ||
@@ -7,3 +8,3 @@ scanPageSize = n; | ||
export function restoreScanPageSizeForTesting() { | ||
scanPageSize = 100; | ||
scanPageSize = defaultScanSize; | ||
} | ||
@@ -14,3 +15,3 @@ /** | ||
export class ScanIterator { | ||
constructor(kind, prefix, start, invoke, getTransaction, shouldCloseTranscation) { | ||
constructor(isWASM, kind, prefix, start, invoke, getTransaction, shouldCloseTranscation) { | ||
this._scanItems = []; | ||
@@ -21,2 +22,3 @@ this._current = 0; | ||
this._transaction = undefined; | ||
this._isWASM = isWASM; | ||
this._kind = kind; | ||
@@ -49,3 +51,3 @@ this._prefix = prefix; | ||
if (!this._moreItemsToLoad) { | ||
return { done: true, value: undefined }; | ||
return this.return(); | ||
} | ||
@@ -96,11 +98,23 @@ this._loadPromise = this._load(); | ||
} | ||
const scanItems = await this._invoke('scan', { | ||
transactionId: this._transaction.id, | ||
const opts = { | ||
prefix: this._prefix, | ||
start, | ||
limit: scanPageSize, | ||
}); | ||
}; | ||
const args = { | ||
transactionId: this._transaction.id, | ||
...(this._isWASM ? { opts } : opts), | ||
}; | ||
const response = await this._invoke('scan', args); | ||
// TODO(repc-switchover): only the !array path is needed for repc. | ||
const scanItems = Array.isArray(response) ? response : response.items; | ||
if (scanItems.length !== scanPageSize) { | ||
this._moreItemsToLoad = false; | ||
} | ||
if (this._isWASM) { | ||
for (const item of scanItems) { | ||
// Temporarily circument the readonly-ness of item.value to parse. | ||
item.value = JSON.parse(item.value); // eslint-disable-line @typescript-eslint/no-explicit-any | ||
} | ||
} | ||
this._scanItems.push(...scanItems); | ||
@@ -110,3 +124,4 @@ } | ||
export class ScanResult { | ||
constructor(...args) { | ||
constructor(isWASM, ...args) { | ||
this._isWASM = isWASM; | ||
this._args = args; | ||
@@ -118,13 +133,13 @@ } | ||
values() { | ||
return this._newIterator('value'); | ||
return this._newIterator(this._isWASM, 'value'); | ||
} | ||
keys() { | ||
return this._newIterator('key'); | ||
return this._newIterator(this._isWASM, 'key'); | ||
} | ||
entries() { | ||
return this._newIterator('entry'); | ||
return this._newIterator(this._isWASM, 'entry'); | ||
} | ||
_newIterator(kind) { | ||
return new ScanIterator(kind, ...this._args); | ||
_newIterator(isWASM, kind) { | ||
return new ScanIterator(isWASM, kind, ...this._args); | ||
} | ||
} |
@@ -5,2 +5,3 @@ import type { ScanBound } from './scan-bound.js'; | ||
start?: ScanBound; | ||
limit?: number; | ||
}; |
@@ -34,5 +34,6 @@ import type { JSONValue, ToJSON } from './json.js'; | ||
private _transactionId; | ||
protected readonly _isWASM: boolean; | ||
protected readonly _invoke: Invoke; | ||
protected _closed: boolean; | ||
constructor(invoke: Invoke); | ||
constructor(isWASM: boolean, invoke: Invoke); | ||
get(key: string): Promise<JSONValue | undefined>; | ||
@@ -39,0 +40,0 @@ has(key: string): Promise<boolean>; |
@@ -9,5 +9,6 @@ import { ScanResult } from './scan-iterator.js'; | ||
export class ReadTransactionImpl { | ||
constructor(invoke) { | ||
constructor(isWASM, invoke) { | ||
this._transactionId = -1; | ||
this._closed = false; | ||
this._isWASM = isWASM; | ||
this._invoke = invoke; | ||
@@ -24,3 +25,3 @@ } | ||
} | ||
return result.value; | ||
return this._isWASM ? JSON.parse(result.value) : result.value; | ||
} | ||
@@ -36,3 +37,3 @@ async has(key) { | ||
scan({ prefix = '', start } = {}) { | ||
return new ScanResult(prefix, start, this._invoke, () => this, false); | ||
return new ScanResult(this._isWASM, prefix, start, this._invoke, () => this, false); | ||
} | ||
@@ -67,3 +68,3 @@ get id() { | ||
key, | ||
value, | ||
value: this._isWASM ? JSON.stringify(value) : value, | ||
}); | ||
@@ -70,0 +71,0 @@ } |
{ | ||
"name": "replicache", | ||
"description": "Offline-First for Every Application", | ||
"version": "0.0.6", | ||
"version": "0.0.7", | ||
"repository": "github:rocicorp/replicache-sdk-js", | ||
@@ -12,8 +12,8 @@ "scripts": { | ||
"doc": "typedoc --name Replicache --mode library --exclude node_modules --exclude src/*.test.ts --excludeNotExported --excludePrivate --excludeProtected --out docs --excludeExternals src/mod.ts", | ||
"build": "tsc", | ||
"build": "tsc && rm -rf out/wasm && mkdir out/wasm && cp -R src/wasm/* out/wasm", | ||
"build:watch": "tsc --watch", | ||
"build:cjs": "tsc --outDir out.cjs --module CommonJS", | ||
"build:cjs": "tsc --outDir out.cjs --module CommonJS && rm -rf out/wasm && mkdir out/wasm && cp -R src/wasm/* out/wasm", | ||
"start:test-server": "bin/test-server --no-fake-time", | ||
"start:diff-server": "bin/diff-server --db=/tmp/diffs serve", | ||
"postinstall": "tool/build.sh", | ||
"postinstall": "tool/get-deps.sh", | ||
"prepublishOnly": "tool/validate-binaries-for-publish.js && npm run lint && npm run test && rm -rf out && npm run build && rm -rf out.cjs && npm run build:cjs" | ||
@@ -40,2 +40,3 @@ }, | ||
"main": "out.cjs/mod.js", | ||
"types": "out/mod.js", | ||
"dependencies": {}, | ||
@@ -45,4 +46,4 @@ "files": [ | ||
"out.cjs", | ||
"tool/build.sh" | ||
"tool/get-deps.sh" | ||
] | ||
} | ||
} |
122
README.md
@@ -7,6 +7,10 @@ # Replicache JS SDK | ||
## Installation | ||
## 👋 Quickstart | ||
You can install Replicache JS SDK from npm. | ||
This tutorial walks through creating a basic offline-first todo app with [Replicache](https://replicache.dev/). If you have any problems or questions, please [join us on Slack](https://join.slack.com/t/rocicorp/shared_invite/zt-h8ygwu8j-RVniv5XsBps0Q9oJXdMyoA). We'd be happy to help. | ||
**Note:** This document assumes you already know what Replicache is, why you might need it, and broadly how it works. If that's not true check out the [design document](https://github.com/rocicorp/replicache/blob/master/design.md) for a detailed deep-dive. | ||
## 🏃♂️ Install | ||
``` | ||
@@ -16,73 +20,79 @@ npm install replicache | ||
## Get Binaries | ||
## 🚴🏿♀️ Instantiate | ||
The binaries are downloaded when you do `npm install`. If for some reason you | ||
need to redownload these you can manually run `tool/build.sh`. Do this again | ||
whenever you update the SDK. | ||
Replicache ships with both ES6 and CommonJS modules. For simplicity, these examples use ES6. | ||
## Run `test-server` | ||
```html | ||
<script type='module'> | ||
import Replicache from './node_modules/replicache/out/mod.js'; | ||
Currently, the JavaScript SDK relies on a native local server that implements | ||
the guts of the sync protocol on the client side. This is temporary and will be | ||
removed. | ||
var rep = new Replicache({ | ||
// URL of the diff server to use. The diff server periodically fetches | ||
// the "client view" from your service and forwards any delta to the | ||
// client. You can use our hosted diff server (as here) or a local diff | ||
// server, which is useful during development. See | ||
// https://github.com/rocicorp/replicache#server-side for more | ||
// information on setting up your client view. | ||
diffServerURL: 'https://serve.replicache.dev/pull', | ||
For now, you must have this server running whenever you are working with the | ||
SDK: | ||
// Auth token for the diff server, if any. | ||
diffServerAuth: '1', | ||
// URL of your service's Replicache batch endpoint. Replicache | ||
// will send batches of mutations here for application. | ||
batchURL: 'https://replicache-sample-todo.now.sh/serve/replicache-batch', | ||
// Auth token for your client view and batch endpoints, if any. | ||
dataLayerAuth: '2', | ||
}); | ||
</script> | ||
``` | ||
mkdir ~/.repm | ||
npx test-server --storage-dir=$HOME/.repm | ||
``` | ||
## Start your Data Layer | ||
## 🚗 Render UI | ||
See [Replicache Server | ||
Setup](https://github.com/rocicorp/replicache#server-side) for server-side | ||
instructions. | ||
Use `subscribe()` to open standing queries. Replicache fires `onData` whenever the result of the query changes, either because of local changes or sync. | ||
For the rest of these instructions we will assume your data layer is running on | ||
`localhost:3000`. | ||
```js | ||
rep.subscribe(async tx => { | ||
return await toArray(tx.scan({ prefix: '/todo/' })); | ||
}, { | ||
onData: result => { | ||
// Using lit-html, but the principle is the same in any UI framework. | ||
// See https://github.com/rocicorp/replicache-sdk-js/tree/master/sample/cal | ||
// for an example using React. | ||
const toggle = complete => html`<td><input type=checkbox .checked=${complete}></td>`; | ||
const title = text => html`<td>${text}</td>`; | ||
const row = todo => html`<tr>${toggle(todo.complete)}${title(todo.text)}</tr>`; | ||
render(html`<table>${result.map(row)}</table>`, document.body); | ||
}, | ||
}); | ||
``` | ||
## Start Diff-Server | ||
## 🏎 Mutate Data | ||
In production, your app will talk to the production Replicache diff-server at | ||
https://serve.replicache.dev/. | ||
Register client-side *mutators* using `register()`. | ||
During development, that server can't reach your workstation, so we provide a | ||
development instance to work against instead. Leave this running in a tab: | ||
Mutators run completely locally, without waiting on the server — online, offline, whatever! A record of the mutation is queued and sent to your service's batch endpoint when possible. | ||
```bash | ||
# The --client-view flag should point to the Client View endpoint | ||
# on your development data layer. | ||
npx diff-server --client-view="http://localhost:3000/replicache-client-view" | ||
``` | ||
Replicache also invokes mutators itself, during sync, to replay unacknowledged changes on top of newly received server state. | ||
## Including the JS | ||
```js | ||
const updateTodo = rep.register('updateTodo', async (tx, { id, complete }) => { | ||
const key = `/todo/${id}`; | ||
const todo = await tx.get(key); | ||
todo.complete = complete; | ||
await tx.put(key, todo); | ||
}); | ||
It is recommended to use ES modules (but we also include CommonJS for backwards | ||
compat). | ||
```js | ||
import Replicache, {REPMHTTPInvoker} from 'replicache'; | ||
const handleCheckbox = async (id, e) => { | ||
await updateTodo({ id, complete: e.srcElement.checked }); | ||
} | ||
``` | ||
To use `Replicache` you currently have to tell it how to invoke the | ||
**Rep**licache Client API **M**odule (REPM). This implementation detail will be hidden/removed in the future. | ||
## 🚀 Next Steps | ||
```js | ||
const diffServerURL = 'https://serve.replicache.dev/pull'; | ||
const diffServerAuth = '<your diff-server account ID>'; | ||
const batchURL = 'https://youservice.com/replicache-batch'; | ||
const dataLayerAuth = '<your data-layer auth token>'; | ||
const repmInvoker = new REPMHTTPInvoker('http://localhost:7002'); | ||
const repmInvoke = repmInvoker.invoke; | ||
const replicache = new Replicache({ | ||
diffServerURL, | ||
diffServerAuth, | ||
batchURL, | ||
dataLayerAuth, | ||
repmInvoke, | ||
}); | ||
const value = await replicache.query(tx => tx.get('/hello')); | ||
console.log(value); | ||
``` | ||
That's it! You've built a fully-functioning offline-first todo app against our sample backend. What will you do next? | ||
* [Learn how to build your own backend integration](https://github.com/rocicorp/replicache#server-side) | ||
* [Check out the richer React/Babel/GCal sample](https://github.com/rocicorp/replicache-sdk-js/tree/master/sample/cal) | ||
* [Browse the full JS documentation](https://replicache-sdk-js.now.sh/) |
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
983498
72
5041
97
6