From 2ee9431f1ba5bf67546bbf85758a01991c40673f Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 15 Jun 2019 22:44:54 +0200 Subject: idb wip --- packages/idb-bridge/src/util/FakeEvent.ts | 80 ++++++++++ packages/idb-bridge/src/util/FakeEventTarget.ts | 177 ++++++++++++++++++++++ packages/idb-bridge/src/util/canInjectKey.ts | 34 +++++ packages/idb-bridge/src/util/cmp.ts | 108 +++++++++++++ packages/idb-bridge/src/util/deepEquals.ts | 75 +++++++++ packages/idb-bridge/src/util/enforceRange.ts | 18 +++ packages/idb-bridge/src/util/errors.ts | 120 +++++++++++++++ packages/idb-bridge/src/util/extractKey.ts | 55 +++++++ packages/idb-bridge/src/util/fakeDOMStringList.ts | 37 +++++ packages/idb-bridge/src/util/injectKey.ts | 48 ++++++ packages/idb-bridge/src/util/makeStoreKeyValue.ts | 92 +++++++++++ packages/idb-bridge/src/util/openPromise.ts | 22 +++ packages/idb-bridge/src/util/queueTask.ts | 21 +++ packages/idb-bridge/src/util/structuredClone.ts | 15 ++ packages/idb-bridge/src/util/types.ts | 73 +++++++++ packages/idb-bridge/src/util/validateKeyPath.ts | 77 ++++++++++ packages/idb-bridge/src/util/valueToKey.ts | 70 +++++++++ 17 files changed, 1122 insertions(+) create mode 100644 packages/idb-bridge/src/util/FakeEvent.ts create mode 100644 packages/idb-bridge/src/util/FakeEventTarget.ts create mode 100644 packages/idb-bridge/src/util/canInjectKey.ts create mode 100644 packages/idb-bridge/src/util/cmp.ts create mode 100644 packages/idb-bridge/src/util/deepEquals.ts create mode 100644 packages/idb-bridge/src/util/enforceRange.ts create mode 100644 packages/idb-bridge/src/util/errors.ts create mode 100644 packages/idb-bridge/src/util/extractKey.ts create mode 100644 packages/idb-bridge/src/util/fakeDOMStringList.ts create mode 100644 packages/idb-bridge/src/util/injectKey.ts create mode 100644 packages/idb-bridge/src/util/makeStoreKeyValue.ts create mode 100644 packages/idb-bridge/src/util/openPromise.ts create mode 100644 packages/idb-bridge/src/util/queueTask.ts create mode 100644 packages/idb-bridge/src/util/structuredClone.ts create mode 100644 packages/idb-bridge/src/util/types.ts create mode 100644 packages/idb-bridge/src/util/validateKeyPath.ts create mode 100644 packages/idb-bridge/src/util/valueToKey.ts (limited to 'packages/idb-bridge/src/util') diff --git a/packages/idb-bridge/src/util/FakeEvent.ts b/packages/idb-bridge/src/util/FakeEvent.ts new file mode 100644 index 000000000..bf17f7e72 --- /dev/null +++ b/packages/idb-bridge/src/util/FakeEvent.ts @@ -0,0 +1,80 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. +*/ + + +import FakeEventTarget from "./FakeEventTarget"; +import { EventType } from "./types"; + +class Event { + public eventPath: FakeEventTarget[] = []; + public type: EventType; + + public readonly NONE = 0; + public readonly CAPTURING_PHASE = 1; + public readonly AT_TARGET = 2; + public readonly BUBBLING_PHASE = 3; + + // Flags + public propagationStopped = false; + public immediatePropagationStopped = false; + public canceled = false; + public initialized = true; + public dispatched = false; + + public target: FakeEventTarget | null = null; + public currentTarget: FakeEventTarget | null = null; + + public eventPhase: 0 | 1 | 2 | 3 = 0; + + public defaultPrevented = false; + + public isTrusted = false; + public timeStamp = Date.now(); + + public bubbles: boolean; + public cancelable: boolean; + + constructor( + type: EventType, + eventInitDict: { bubbles?: boolean; cancelable?: boolean } = {}, + ) { + this.type = type; + + this.bubbles = + eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; + this.cancelable = + eventInitDict.cancelable !== undefined + ? eventInitDict.cancelable + : false; + } + + public preventDefault() { + if (this.cancelable) { + this.canceled = true; + } + } + + public stopPropagation() { + this.propagationStopped = true; + } + + public stopImmediatePropagation() { + this.propagationStopped = true; + this.immediatePropagationStopped = true; + } +} + +export default Event; diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts b/packages/idb-bridge/src/util/FakeEventTarget.ts new file mode 100644 index 000000000..3c7eaf468 --- /dev/null +++ b/packages/idb-bridge/src/util/FakeEventTarget.ts @@ -0,0 +1,177 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. + */ + + +import { InvalidStateError } from "./errors"; +import FakeEvent from "./FakeEvent"; +import { EventCallback, EventType } from "./types"; + +type EventTypeProp = + | "onabort" + | "onblocked" + | "oncomplete" + | "onerror" + | "onsuccess" + | "onupgradeneeded" + | "onversionchange"; + +interface Listener { + callback: EventCallback; + capture: boolean; + type: EventType; +} + +const stopped = (event: FakeEvent, listener: Listener) => { + return ( + event.immediatePropagationStopped || + (event.eventPhase === event.CAPTURING_PHASE && + listener.capture === false) || + (event.eventPhase === event.BUBBLING_PHASE && listener.capture === true) + ); +}; + +// http://www.w3.org/TR/dom/#concept-event-listener-invoke +const invokeEventListeners = (event: FakeEvent, obj: FakeEventTarget) => { + event.currentTarget = obj; + + // The callback might cause obj.listeners to mutate as we traverse it. + // Take a copy of the array so that nothing sneaks in and we don't lose + // our place. + for (const listener of obj.listeners.slice()) { + if (event.type !== listener.type || stopped(event, listener)) { + continue; + } + + // @ts-ignore + listener.callback.call(event.currentTarget, event); + } + + const typeToProp: { [key in EventType]: EventTypeProp } = { + abort: "onabort", + blocked: "onblocked", + complete: "oncomplete", + error: "onerror", + success: "onsuccess", + upgradeneeded: "onupgradeneeded", + versionchange: "onversionchange", + }; + const prop = typeToProp[event.type]; + if (prop === undefined) { + throw new Error(`Unknown event type: "${event.type}"`); + } + + const callback = event.currentTarget[prop]; + if (callback) { + const listener = { + callback, + capture: false, + type: event.type, + }; + if (!stopped(event, listener)) { + // @ts-ignore + listener.callback.call(event.currentTarget, event); + } + } +}; + +abstract class FakeEventTarget { + public readonly listeners: Listener[] = []; + + // These will be overridden in individual subclasses and made not readonly + public readonly onabort: EventCallback | null | undefined; + public readonly onblocked: EventCallback | null | undefined; + public readonly oncomplete: EventCallback | null | undefined; + public readonly onerror: EventCallback | null | undefined; + public readonly onsuccess: EventCallback | null | undefined; + public readonly onupgradeneeded: EventCallback | null | undefined; + public readonly onversionchange: EventCallback | null | undefined; + + public addEventListener( + type: EventType, + callback: EventCallback, + capture = false, + ) { + this.listeners.push({ + callback, + capture, + type, + }); + } + + public removeEventListener( + type: EventType, + callback: EventCallback, + capture = false, + ) { + const i = this.listeners.findIndex(listener => { + return ( + listener.type === type && + listener.callback === callback && + listener.capture === capture + ); + }); + + this.listeners.splice(i, 1); + } + + // http://www.w3.org/TR/dom/#dispatching-events + public dispatchEvent(event: FakeEvent) { + if (event.dispatched || !event.initialized) { + throw new InvalidStateError("The object is in an invalid state."); + } + event.isTrusted = false; + + event.dispatched = true; + event.target = this; + // NOT SURE WHEN THIS SHOULD BE SET event.eventPath = []; + + event.eventPhase = event.CAPTURING_PHASE; + for (const obj of event.eventPath) { + if (!event.propagationStopped) { + invokeEventListeners(event, obj); + } + } + + event.eventPhase = event.AT_TARGET; + if (!event.propagationStopped) { + invokeEventListeners(event, event.target); + } + + if (event.bubbles) { + event.eventPath.reverse(); + event.eventPhase = event.BUBBLING_PHASE; + if (event.eventPath.length === 0 && event.type === "error") { + console.error("Unhandled error event: ", event.target); + } + for (const obj of event.eventPath) { + if (!event.propagationStopped) { + invokeEventListeners(event, obj); + } + } + } + + event.dispatched = false; + event.eventPhase = event.NONE; + event.currentTarget = null; + + if (event.canceled) { + return false; + } + return true; + } +} + +export default FakeEventTarget; diff --git a/packages/idb-bridge/src/util/canInjectKey.ts b/packages/idb-bridge/src/util/canInjectKey.ts new file mode 100644 index 000000000..c6c9c24ab --- /dev/null +++ b/packages/idb-bridge/src/util/canInjectKey.ts @@ -0,0 +1,34 @@ +import { KeyPath, Value } from "./types"; + +// http://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value +const canInjectKey = (keyPath: KeyPath, value: Value) => { + if (Array.isArray(keyPath)) { + // tslint:disable-next-line max-line-length + throw new Error( + "The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.", + ); + } + + const identifiers = keyPath.split("."); + if (identifiers.length === 0) { + throw new Error("Assert: identifiers is not empty"); + } + identifiers.pop(); + + for (const identifier of identifiers) { + if (typeof value !== "object" && !Array.isArray(value)) { + return false; + } + + const hop = value.hasOwnProperty(identifier); + if (!hop) { + return true; + } + + value = value[identifier]; + } + + return typeof value === "object" || Array.isArray(value); +}; + +export default canInjectKey; diff --git a/packages/idb-bridge/src/util/cmp.ts b/packages/idb-bridge/src/util/cmp.ts new file mode 100644 index 000000000..9d0dc99a2 --- /dev/null +++ b/packages/idb-bridge/src/util/cmp.ts @@ -0,0 +1,108 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. + */ + +import { DataError } from "./errors"; +import valueToKey from "./valueToKey"; + +const getType = (x: any) => { + if (typeof x === "number") { + return "Number"; + } + if (x instanceof Date) { + return "Date"; + } + if (Array.isArray(x)) { + return "Array"; + } + if (typeof x === "string") { + return "String"; + } + if (x instanceof ArrayBuffer) { + return "Binary"; + } + + throw new DataError(); +}; + +// https://w3c.github.io/IndexedDB/#compare-two-keys +const compareKeys = (first: any, second: any): -1 | 0 | 1 => { + if (second === undefined) { + throw new TypeError(); + } + + first = valueToKey(first); + second = valueToKey(second); + + const t1 = getType(first); + const t2 = getType(second); + + if (t1 !== t2) { + if (t1 === "Array") { + return 1; + } + if ( + t1 === "Binary" && + (t2 === "String" || t2 === "Date" || t2 === "Number") + ) { + return 1; + } + if (t1 === "String" && (t2 === "Date" || t2 === "Number")) { + return 1; + } + if (t1 === "Date" && t2 === "Number") { + return 1; + } + return -1; + } + + if (t1 === "Binary") { + first = new Uint8Array(first); + second = new Uint8Array(second); + } + + if (t1 === "Array" || t1 === "Binary") { + const length = Math.min(first.length, second.length); + for (let i = 0; i < length; i++) { + const result = compareKeys(first[i], second[i]); + + if (result !== 0) { + return result; + } + } + + if (first.length > second.length) { + return 1; + } + if (first.length < second.length) { + return -1; + } + return 0; + } + + if (t1 === "Date") { + if (first.getTime() === second.getTime()) { + return 0; + } + } else { + if (first === second) { + return 0; + } + } + + return first > second ? 1 : -1; +}; + +export default compareKeys; diff --git a/packages/idb-bridge/src/util/deepEquals.ts b/packages/idb-bridge/src/util/deepEquals.ts new file mode 100644 index 000000000..8d05ad21e --- /dev/null +++ b/packages/idb-bridge/src/util/deepEquals.ts @@ -0,0 +1,75 @@ +/* +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + + + + +const isArray = Array.isArray; +const keyList = Object.keys; +const hasProp = Object.prototype.hasOwnProperty; + +function deepEquals(a: any, b: any): boolean { + if (a === b) return true; + + if (a && b && typeof a == "object" && typeof b == "object") { + const arrA = isArray(a); + const arrB = isArray(b); + let i; + let length; + let key; + + if (arrA && arrB) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0; ) if (!deepEquals(a[i], b[i])) return false; + return true; + } + + if (arrA != arrB) return false; + + const dateA = a instanceof Date; + const dateB = b instanceof Date; + if (dateA != dateB) return false; + if (dateA && dateB) return a.getTime() == b.getTime(); + + const regexpA = a instanceof RegExp; + const regexpB = b instanceof RegExp; + if (regexpA != regexpB) return false; + if (regexpA && regexpB) return a.toString() == b.toString(); + + const keys = keyList(a); + length = keys.length; + + if (length !== keyList(b).length) return false; + + for (i = length; i-- !== 0; ) if (!hasProp.call(b, keys[i])) return false; + + for (i = length; i-- !== 0; ) { + key = keys[i]; + if (!deepEquals(a[key], b[key])) return false; + } + + return true; + } + + return a !== a && b !== b; +} diff --git a/packages/idb-bridge/src/util/enforceRange.ts b/packages/idb-bridge/src/util/enforceRange.ts new file mode 100644 index 000000000..0cf3b6c85 --- /dev/null +++ b/packages/idb-bridge/src/util/enforceRange.ts @@ -0,0 +1,18 @@ +// https://heycam.github.io/webidl/#EnforceRange + +const enforceRange = ( + num: number, + type: "MAX_SAFE_INTEGER" | "unsigned long", +) => { + const min = 0; + const max = type === "unsigned long" ? 4294967295 : 9007199254740991; + + if (isNaN(num) || num < min || num > max) { + throw new TypeError(); + } + if (num >= 0) { + return Math.floor(num); + } +}; + +export default enforceRange; diff --git a/packages/idb-bridge/src/util/errors.ts b/packages/idb-bridge/src/util/errors.ts new file mode 100644 index 000000000..bbf9498c1 --- /dev/null +++ b/packages/idb-bridge/src/util/errors.ts @@ -0,0 +1,120 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. + */ + + +/* tslint:disable: max-classes-per-file max-line-length */ + +const messages = { + AbortError: + "A request was aborted, for example through a call to IDBTransaction.abort.", + ConstraintError: + "A mutation operation in the transaction failed because a constraint was not satisfied. For example, an object such as an object store or index already exists and a request attempted to create a new one.", + DataCloneError: + "The data being stored could not be cloned by the internal structured cloning algorithm.", + DataError: "Data provided to an operation does not meet requirements.", + InvalidAccessError: + "An invalid operation was performed on an object. For example transaction creation attempt was made, but an empty scope was provided.", + InvalidStateError: + "An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.", + NotFoundError: + "The operation failed because the requested database object could not be found. For example, an object store did not exist but was being opened.", + ReadOnlyError: + 'The mutating operation was attempted in a "readonly" transaction.', + TransactionInactiveError: + "A request was placed against a transaction which is currently not active, or which is finished.", + VersionError: + "An attempt was made to open a database using a lower version than the existing version.", +}; + +export class AbortError extends Error { + constructor(message = messages.AbortError) { + super(); + this.name = "AbortError"; + this.message = message; + } +} + +export class ConstraintError extends Error { + constructor(message = messages.ConstraintError) { + super(); + this.name = "ConstraintError"; + this.message = message; + } +} + +export class DataCloneError extends Error { + constructor(message = messages.DataCloneError) { + super(); + this.name = "DataCloneError"; + this.message = message; + } +} + +export class DataError extends Error { + constructor(message = messages.DataError) { + super(); + this.name = "DataError"; + this.message = message; + } +} + +export class InvalidAccessError extends Error { + constructor(message = messages.InvalidAccessError) { + super(); + this.name = "InvalidAccessError"; + this.message = message; + } +} + +export class InvalidStateError extends Error { + constructor(message = messages.InvalidStateError) { + super(); + this.name = "InvalidStateError"; + this.message = message; + } +} + +export class NotFoundError extends Error { + constructor(message = messages.NotFoundError) { + super(); + this.name = "NotFoundError"; + this.message = message; + } +} + +export class ReadOnlyError extends Error { + constructor(message = messages.ReadOnlyError) { + super(); + this.name = "ReadOnlyError"; + this.message = message; + } +} + +export class TransactionInactiveError extends Error { + constructor(message = messages.TransactionInactiveError) { + super(); + this.name = "TransactionInactiveError"; + this.message = message; + } +} + +export class VersionError extends Error { + constructor(message = messages.VersionError) { + super(); + this.name = "VersionError"; + this.message = message; + } +} diff --git a/packages/idb-bridge/src/util/extractKey.ts b/packages/idb-bridge/src/util/extractKey.ts new file mode 100644 index 000000000..fd14c5a64 --- /dev/null +++ b/packages/idb-bridge/src/util/extractKey.ts @@ -0,0 +1,55 @@ +import { Key, KeyPath, Value } from "./types"; +import valueToKey from "./valueToKey"; + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-extracting-a-key-from-a-value-using-a-key-path +const extractKey = (keyPath: KeyPath, value: Value) => { + if (Array.isArray(keyPath)) { + const result: Key[] = []; + + for (let item of keyPath) { + // This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same + // comment in validateKeyPath) + if ( + item !== undefined && + item !== null && + typeof item !== "string" && + (item as any).toString + ) { + item = (item as any).toString(); + } + result.push(valueToKey(extractKey(item, value))); + } + + return result; + } + + if (keyPath === "") { + return value; + } + + let remainingKeyPath: string | null = keyPath; + let object = value; + + while (remainingKeyPath !== null) { + let identifier; + + const i = remainingKeyPath.indexOf("."); + if (i >= 0) { + identifier = remainingKeyPath.slice(0, i); + remainingKeyPath = remainingKeyPath.slice(i + 1); + } else { + identifier = remainingKeyPath; + remainingKeyPath = null; + } + + if (!object.hasOwnProperty(identifier)) { + return; + } + + object = object[identifier]; + } + + return object; +}; + +export default extractKey; diff --git a/packages/idb-bridge/src/util/fakeDOMStringList.ts b/packages/idb-bridge/src/util/fakeDOMStringList.ts new file mode 100644 index 000000000..5add17588 --- /dev/null +++ b/packages/idb-bridge/src/util/fakeDOMStringList.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Jeremy Scheff + * + * 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. + */ + +import { FakeDOMStringList } from "./types"; + +// Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that. + +const fakeDOMStringList = (arr: string[]): FakeDOMStringList => { + const arr2 = arr.slice(); + + Object.defineProperty(arr2, "contains", { + // tslint:disable-next-line object-literal-shorthand + value: (value: string) => arr2.indexOf(value) >= 0, + }); + + Object.defineProperty(arr2, "item", { + // tslint:disable-next-line object-literal-shorthand + value: (i: number) => arr2[i], + }); + + return arr2 as FakeDOMStringList; +}; + +export default fakeDOMStringList; diff --git a/packages/idb-bridge/src/util/injectKey.ts b/packages/idb-bridge/src/util/injectKey.ts new file mode 100644 index 000000000..89d3a01d8 --- /dev/null +++ b/packages/idb-bridge/src/util/injectKey.ts @@ -0,0 +1,48 @@ +import { KeyPath, Value, Key } from "./types"; +import canInjectKey from "./canInjectKey"; +import { DataError } from "./errors"; +import structuredClone from "./structuredClone"; + +export function injectKey(keyPath: KeyPath, value: Value, key: Key): Value { + if (Array.isArray(keyPath)) { + // tslint:disable-next-line max-line-length + throw new Error( + "The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.", + ); + } + + const identifiers = keyPath.split("."); + if (identifiers.length === 0) { + throw new Error("Assert: identifiers is not empty"); + } + + const lastIdentifier = identifiers.pop(); + + if (lastIdentifier === null || lastIdentifier === undefined) { + throw Error(); + } + + for (const identifier of identifiers) { + if (typeof value !== "object" && !Array.isArray(value)) { + return false; + } + + const hop = value.hasOwnProperty(identifier); + if (!hop) { + return true; + } + + value = value[identifier]; + } + + if (!(typeof value === "object" || Array.isArray(value))) { + throw new Error("can't inject key"); + } + + const newValue = structuredClone(value); + newValue[lastIdentifier] = structuredClone(key); + + return newValue; +} + +export default injectKey; \ No newline at end of file diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts new file mode 100644 index 000000000..4850cec26 --- /dev/null +++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts @@ -0,0 +1,92 @@ +import { Value, Key, KeyPath } from "./types"; +import extractKey from "./extractKey"; +import { DataError } from "./errors"; +import valueToKey from "./valueToKey"; +import structuredClone from "./structuredClone"; +import injectKey from "./injectKey"; + +export interface StoreKeyResult { + updatedKeyGenerator: number; + key: Key; + value: Value; +} + +export function makeStoreKeyValue( + value: Value, + key: Key | undefined, + currentKeyGenerator: number, + autoIncrement: boolean, + keyPath: KeyPath | null, +): StoreKeyResult { + const haveKey = key !== undefined && key !== null; + const haveKeyPath = keyPath !== null && keyPath !== undefined; + + // This models a decision table on (haveKey, haveKeyPath, autoIncrement) + + value = structuredClone(value); + + if (haveKey) { + if (haveKeyPath) { + // (yes, yes, no) + // (yes, yes, yes) + throw new DataError(); + } else { + if (autoIncrement) { + // (yes, no, yes) + key = valueToKey(key)!; + let updatedKeyGenerator: number; + if (typeof key !== "number") { + updatedKeyGenerator = currentKeyGenerator; + } else { + updatedKeyGenerator = key; + } + return { + key: key!, + value: value, + updatedKeyGenerator, + }; + } else { + // (yes, no, no) + throw new DataError(); + } + } + } else { + if (haveKeyPath) { + if (autoIncrement) { + // (no, yes, yes) + + let updatedKeyGenerator: number; + const maybeInlineKey = extractKey(keyPath!, value); + if (maybeInlineKey === undefined) { + value = injectKey(keyPath!, value, currentKeyGenerator); + key = currentKeyGenerator; + updatedKeyGenerator = currentKeyGenerator + 1; + } else if (typeof maybeInlineKey === "number") { + key = maybeInlineKey; + updatedKeyGenerator = maybeInlineKey; + } else { + key = maybeInlineKey; + updatedKeyGenerator = currentKeyGenerator + 1; + } + return { + key: key, + value: value, + updatedKeyGenerator, + } + } else { + // (no, yes, no) + key = extractKey(keyPath!, value); + key = valueToKey(key); + return { + key: key!, + value: value, + updatedKeyGenerator: currentKeyGenerator, + }; + } + } else { + // (no, no, yes) + // (no, no, no) + throw new DataError(); + } + } +} \ No newline at end of file diff --git a/packages/idb-bridge/src/util/openPromise.ts b/packages/idb-bridge/src/util/openPromise.ts new file mode 100644 index 000000000..3f6da81bd --- /dev/null +++ b/packages/idb-bridge/src/util/openPromise.ts @@ -0,0 +1,22 @@ +function openPromise(): { + promise: Promise; + resolve: (v?: T | PromiseLike) => void; + reject: (err?: any) => void; +} { + let resolve; + let reject; + const promise = new Promise((resolve2, reject2) => { + resolve = resolve2; + reject = reject2; + }); + if (!resolve) { + throw Error("broken invariant"); + } + if (!reject) { + throw Error("broken invariant"); + } + + return { promise, resolve, reject }; +} + +export default openPromise; diff --git a/packages/idb-bridge/src/util/queueTask.ts b/packages/idb-bridge/src/util/queueTask.ts new file mode 100644 index 000000000..7d59c2263 --- /dev/null +++ b/packages/idb-bridge/src/util/queueTask.ts @@ -0,0 +1,21 @@ +/* + Copyright 2019 Florian Dold + + 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. + */ + + export function queueTask(fn: () => void) { + setImmediate(fn); + } + + export default queueTask; \ No newline at end of file diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts new file mode 100644 index 000000000..8b6b61315 --- /dev/null +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -0,0 +1,15 @@ + +function structuredCloneImpl(val: any, visited: WeakMap): any { + // FIXME: replace with real implementation! + return JSON.parse(JSON.stringify(val)); +} + +/** + * Structured clone for IndexedDB. + */ +export function structuredClone(val: any): any { + const visited: WeakMap = new WeakMap(); + return structuredCloneImpl(val, visited); +} + +export default structuredClone; \ No newline at end of file diff --git a/packages/idb-bridge/src/util/types.ts b/packages/idb-bridge/src/util/types.ts new file mode 100644 index 000000000..7aea22ab1 --- /dev/null +++ b/packages/idb-bridge/src/util/types.ts @@ -0,0 +1,73 @@ +/* + Copyright 2017 Jeremy Scheff + Copyright 2019 Florian Dold + + 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. +*/ + +import BridgeIDBRequest from "../BridgeIDBRequest"; +import BridgeIDBKeyRange from "../BridgeIDBKeyRange"; +import BridgeIDBIndex from "../BridgeIDBIndex"; +import BridgeIBObjectStore from "../BridgeIDBObjectStore"; + +interface EventInCallback extends Event { + target: any; + error: Error | null; +} + +export type EventCallback = (event: EventInCallback) => void; + +export type EventType = + | "abort" + | "blocked" + | "complete" + | "error" + | "success" + | "upgradeneeded" + | "versionchange"; + +export type CursorSource = BridgeIDBIndex | BridgeIBObjectStore; + + +export interface FakeDOMStringList extends Array { + contains: (value: string) => boolean; + item: (i: number) => string | undefined; +} + +export type BridgeIDBCursorDirection = "next" | "nextunique" | "prev" | "prevunique"; + +export type KeyPath = string | string[]; + +export type Key = any; + +export type CursorRange = Key | BridgeIDBKeyRange | undefined; + +export type Value = any; + +export interface Record { + key: Key; + value: Key | Value; // For indexes, will be Key. For object stores, will be Value. +} + +export type TransactionMode = "readonly" | "readwrite" | "versionchange"; + +export interface BridgeIDBDatabaseInfo { + name: string; + version: number +}; + +export interface RequestObj { + operation: () => Promise; + request?: BridgeIDBRequest | undefined; + source?: any; +} diff --git a/packages/idb-bridge/src/util/validateKeyPath.ts b/packages/idb-bridge/src/util/validateKeyPath.ts new file mode 100644 index 000000000..18552a5d4 --- /dev/null +++ b/packages/idb-bridge/src/util/validateKeyPath.ts @@ -0,0 +1,77 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. +*/ + +import { KeyPath } from "./types"; + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-valid-key-path +const validateKeyPath = (keyPath: KeyPath, parent?: "array" | "string") => { + // This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same + // comment in extractKey) + if ( + keyPath !== undefined && + keyPath !== null && + typeof keyPath !== "string" && + keyPath.toString && + (parent === "array" || !Array.isArray(keyPath)) + ) { + keyPath = keyPath.toString(); + } + + if (typeof keyPath === "string") { + if (keyPath === "" && parent !== "string") { + return; + } + try { + // https://mathiasbynens.be/demo/javascript-identifier-regex for ECMAScript 5.1 / Unicode v7.0.0, with + // reserved words at beginning removed + // tslint:disable-next-line max-line-length + const validIdentifierRegex = /^(?:[\$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B2\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[\$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B2\u08E4-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA69D\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2D\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/; + if (keyPath.length >= 1 && validIdentifierRegex.test(keyPath)) { + return; + } + } catch (err) { + throw new SyntaxError(err.message); + } + if (keyPath.indexOf(" ") >= 0) { + throw new SyntaxError( + "The keypath argument contains an invalid key path (no spaces allowed).", + ); + } + } + + if (Array.isArray(keyPath) && keyPath.length > 0) { + if (parent) { + // No nested arrays + throw new SyntaxError( + "The keypath argument contains an invalid key path (nested arrays).", + ); + } + for (const part of keyPath) { + validateKeyPath(part, "array"); + } + return; + } else if (typeof keyPath === "string" && keyPath.indexOf(".") >= 0) { + keyPath = keyPath.split("."); + for (const part of keyPath) { + validateKeyPath(part, "string"); + } + return; + } + + throw new SyntaxError(); +}; + +export default validateKeyPath; diff --git a/packages/idb-bridge/src/util/valueToKey.ts b/packages/idb-bridge/src/util/valueToKey.ts new file mode 100644 index 000000000..3a9e36786 --- /dev/null +++ b/packages/idb-bridge/src/util/valueToKey.ts @@ -0,0 +1,70 @@ +/* + Copyright 2017 Jeremy Scheff + + 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. + */ + + +import { DataError } from "./errors"; +import { Key } from "./types"; + +// https://w3c.github.io/IndexedDB/#convert-a-value-to-a-input +function valueToKey(input: any, seen?: Set): Key | Key[] { + if (typeof input === "number") { + if (isNaN(input)) { + throw new DataError(); + } + return input; + } else if (input instanceof Date) { + const ms = input.valueOf(); + if (isNaN(ms)) { + throw new DataError(); + } + return new Date(ms); + } else if (typeof input === "string") { + return input; + } else if ( + input instanceof ArrayBuffer || + (typeof ArrayBuffer !== "undefined" && + ArrayBuffer.isView && + ArrayBuffer.isView(input)) + ) { + if (input instanceof ArrayBuffer) { + return new Uint8Array(input).buffer; + } + return new Uint8Array(input.buffer).buffer; + } else if (Array.isArray(input)) { + if (seen === undefined) { + seen = new Set(); + } else if (seen.has(input)) { + throw new DataError(); + } + seen.add(input); + + const keys = []; + for (let i = 0; i < input.length; i++) { + const hop = input.hasOwnProperty(i); + if (!hop) { + throw new DataError(); + } + const entry = input[i]; + const key = valueToKey(entry, seen); + keys.push(key); + } + return keys; + } else { + throw new DataError(); + } +}; + +export default valueToKey; -- cgit v1.2.3