diff --git a/packages/infra/client/Cargo.lock b/packages/infra/client/Cargo.lock index 8b4366c14f..77269f9f42 100644 --- a/packages/infra/client/Cargo.lock +++ b/packages/infra/client/Cargo.lock @@ -4746,6 +4746,7 @@ dependencies = [ "deno_core", "foundationdb", "futures-util", + "indexmap 2.6.0", "pegboard", "prost 0.13.3", "serde", diff --git a/packages/infra/client/actor-kv/Cargo.toml b/packages/infra/client/actor-kv/Cargo.toml index 521e4ce165..b4bf0da971 100644 --- a/packages/infra/client/actor-kv/Cargo.toml +++ b/packages/infra/client/actor-kv/Cargo.toml @@ -10,14 +10,15 @@ anyhow.workspace = true deno_core.workspace = true foundationdb = {version = "0.9.1", features = [ "fdb-7_1", "embedded-fdb-include" ] } futures-util = { version = "0.3" } +indexmap = { version = "2.0" } prost = "0.13.3" serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" -tokio.workspace = true tokio-tungstenite = "0.23.1" -tracing.workspace = true +tokio.workspace = true tracing-logfmt.workspace = true tracing-subscriber.workspace = true +tracing.workspace = true uuid = { version = "1.6.1", features = ["v4"] } pegboard = { path = "../../../services/pegboard", default-features = false } diff --git a/packages/infra/client/actor-kv/src/lib.rs b/packages/infra/client/actor-kv/src/lib.rs index fa257c20dc..e5678013cc 100644 --- a/packages/infra/client/actor-kv/src/lib.rs +++ b/packages/infra/client/actor-kv/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - collections::{hash_map, HashMap}, + collections::HashMap, result::Result::{Err, Ok}, }; @@ -15,6 +15,7 @@ pub use list_query::ListQuery; pub use metadata::Metadata; use pegboard::protocol; use prost::Message; +use indexmap::IndexMap; use utils::{owner_segment, validate_entries, validate_keys, TransactionExt}; mod entry; @@ -174,7 +175,7 @@ impl ActorKv { query: ListQuery, reverse: bool, limit: Option, - ) -> Result> { + ) -> Result> { let subspace = self .subspace .as_ref() @@ -231,13 +232,13 @@ impl ActorKv { // With a limit, we short circuit out of the `try_fold` once the limit is reached if let Some(limit) = limit { stream - .try_fold(HashMap::new(), |mut acc, (key, sub_key)| async { + .try_fold(IndexMap::new(), |mut acc, (key, sub_key)| async { let size = acc.len(); let entry = acc.entry(key); // Short circuit when limit is reached. This relies on data from the stream // being in order. - if size == limit && matches!(entry, hash_map::Entry::Vacant(_)) { + if size == limit && matches!(entry, indexmap::map::Entry::Vacant(_)) { return Err(ListLimitReached(acc).into()); } @@ -255,7 +256,7 @@ impl ActorKv { }) } else { stream - .try_fold(HashMap::new(), |mut acc, (key, sub_key)| async { + .try_fold(IndexMap::new(), |mut acc, (key, sub_key)| async { acc.entry(key) .or_insert_with(EntryBuilder::default) .add_sub_key(sub_key)?; diff --git a/packages/infra/client/actor-kv/src/list_query.rs b/packages/infra/client/actor-kv/src/list_query.rs index 933d302fc0..7b3fb33d01 100644 --- a/packages/infra/client/actor-kv/src/list_query.rs +++ b/packages/infra/client/actor-kv/src/list_query.rs @@ -1,8 +1,7 @@ -use std::collections::HashMap; - use anyhow::*; use foundationdb::tuple::Subspace; use serde::Deserialize; +use indexmap::IndexMap; use crate::{ entry::EntryBuilder, @@ -71,7 +70,7 @@ impl ListQuery { } // Used to short circuit after the -pub struct ListLimitReached(pub HashMap); +pub struct ListLimitReached(pub IndexMap); impl std::fmt::Debug for ListLimitReached { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/packages/infra/client/isolate-v8-runner/js/40_rivet_kv.js b/packages/infra/client/isolate-v8-runner/js/40_rivet_kv.js index 4b2bdb2f3f..63f810b171 100644 --- a/packages/infra/client/isolate-v8-runner/js/40_rivet_kv.js +++ b/packages/infra/client/isolate-v8-runner/js/40_rivet_kv.js @@ -4,6 +4,7 @@ import { core } from "ext:core/mod.js"; import { op_rivet_kv_delete, op_rivet_kv_delete_all, op_rivet_kv_delete_batch, op_rivet_kv_get, op_rivet_kv_get_batch, op_rivet_kv_list, op_rivet_kv_put, op_rivet_kv_put_batch, } from "ext:core/ops"; +import { deepEqual } from "./lib/fast-equals/index.js"; /** * Retrieves a value from the key-value store. */ @@ -18,19 +19,17 @@ async function get(key, options) { */ async function getBatch(keys, options) { const entries = await op_rivet_kv_get_batch(keys.map((x) => serializeKey(x))); - const deserializedValues = new Map(); - for (const [key, entry] of entries) { - const jsKey = deserializeKey(key); - deserializedValues.set(jsKey, deserializeValue(jsKey, entry.value, options?.format)); - } - return deserializedValues; + return new HashMap(entries.map(([key, entry]) => { + let jsKey = deserializeKey(key); + return [jsKey, deserializeValue(jsKey, entry.value, options?.format)]; + })); } /** * Retrieves all key-value pairs in the KV store. When using any of the options, the keys lexicographic order * is used for filtering. * * @param {ListOptions} [options] - Options. - * @returns {Promise>} The retrieved values. + * @returns {Promise>} The retrieved values. */ async function list(options) { // Build query @@ -69,12 +68,10 @@ async function list(options) { query = { all: {} }; } const entries = await op_rivet_kv_list(query, options?.reverse ?? false, options?.limit); - const deserializedValues = new Map(); - for (const [key, entry] of entries) { - const jsKey = deserializeKey(key); - deserializedValues.set(jsKey, deserializeValue(jsKey, entry.value, options?.format)); - } - return deserializedValues; + return new HashMap(entries.map(([key, entry]) => { + let jsKey = deserializeKey(key); + return [jsKey, deserializeValue(jsKey, entry.value, options?.format)]; + })); } /** * Stores a key-value pair in the key-value store. @@ -233,6 +230,35 @@ function deserializeValue(key, value, format = "value") { throw Error(`invalid format: "${format}". expected "value" or "arrayBuffer".`); } } +class HashMap { + #internal; + constructor(internal) { + this.#internal = internal; + } + get(key) { + for (let [k, v] of this.#internal) { + if (deepEqual(key, k)) + return v; + } + return undefined; + } + /** + * Returns a map of keys to values. **WARNING** Using `.get` on the returned map does not work as expected + * with complex types (arrays, objects, etc). Use `.get` on this class instead. + */ + raw() { + return new Map(this.#internal); + } + array() { + return this.#internal; + } + entries() { + return this[Symbol.iterator](); + } + [Symbol.iterator]() { + return this.#internal[Symbol.iterator](); + } +} export const KV_NAMESPACE = { get, getBatch, diff --git a/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/comparator.js b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/comparator.js new file mode 100644 index 0000000000..eaa95ee2b2 --- /dev/null +++ b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/comparator.js @@ -0,0 +1,222 @@ +// DO NOT MODIFY +// +// Generated with scripts/sdk_actor/compile_bridge.ts + +import { areArraysEqual as areArraysEqualDefault, areDatesEqual as areDatesEqualDefault, areMapsEqual as areMapsEqualDefault, areObjectsEqual as areObjectsEqualDefault, areObjectsEqualStrict as areObjectsEqualStrictDefault, arePrimitiveWrappersEqual as arePrimitiveWrappersEqualDefault, areRegExpsEqual as areRegExpsEqualDefault, areSetsEqual as areSetsEqualDefault, areTypedArraysEqual, } from "./equals.js"; +import { combineComparators, createIsCircular } from "./utils.js"; +const ARGUMENTS_TAG = "[object Arguments]"; +const BOOLEAN_TAG = "[object Boolean]"; +const DATE_TAG = "[object Date]"; +const MAP_TAG = "[object Map]"; +const NUMBER_TAG = "[object Number]"; +const OBJECT_TAG = "[object Object]"; +const REG_EXP_TAG = "[object RegExp]"; +const SET_TAG = "[object Set]"; +const STRING_TAG = "[object String]"; +const { isArray } = Array; +const isTypedArray = typeof ArrayBuffer === "function" && ArrayBuffer.isView + ? ArrayBuffer.isView + : null; +const { assign } = Object; +const getTag = Object.prototype.toString.call.bind(Object.prototype.toString); +/** + * Create a comparator method based on the type-specific equality comparators passed. + */ +export function createEqualityComparator({ areArraysEqual, areDatesEqual, areMapsEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, }) { + /** + * compare the value of the two objects and return true if they are equivalent in values + */ + return function comparator(a, b, state) { + // If the items are strictly equal, no need to do a value comparison. + if (a === b) { + return true; + } + // If the items are not non-nullish objects, then the only possibility + // of them being equal but not strictly is if they are both `NaN`. Since + // `NaN` is uniquely not equal to itself, we can use self-comparison of + // both objects, which is faster than `isNaN()`. + if (a == null || + b == null || + typeof a !== "object" || + typeof b !== "object") { + return a !== a && b !== b; + } + const constructor = a.constructor; + // Checks are listed in order of commonality of use-case: + // 1. Common complex object types (plain object, array) + // 2. Common data values (date, regexp) + // 3. Less-common complex object types (map, set) + // 4. Less-common data values (promise, primitive wrappers) + // Inherently this is both subjective and assumptive, however + // when reviewing comparable libraries in the wild this order + // appears to be generally consistent. + // Constructors should match, otherwise there is potential for false positives + // between class and subclass or custom object and POJO. + if (constructor !== b.constructor) { + return false; + } + // `isPlainObject` only checks against the object"s own realm. Cross-realm + // comparisons are rare, and will be handled in the ultimate fallback, so + // we can avoid capturing the string tag. + if (constructor === Object) { + return areObjectsEqual(a, b, state); + } + // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing + // the string tag or doing an `instanceof` check. + if (isArray(a)) { + return areArraysEqual(a, b, state); + } + // `isTypedArray()` works on all possible TypedArray classes, so we can avoid + // capturing the string tag or comparing against all possible constructors. + if (isTypedArray != null && isTypedArray(a)) { + return areTypedArraysEqual(a, b, state); + } + // Try to fast-path equality checks for other complex object types in the + // same realm to avoid capturing the string tag. Strict equality is used + // instead of `instanceof` because it is more performant for the common + // use-case. If someone is subclassing a native class, it will be handled + // with the string tag comparison. + if (constructor === Date) { + return areDatesEqual(a, b, state); + } + if (constructor === RegExp) { + return areRegExpsEqual(a, b, state); + } + if (constructor === Map) { + return areMapsEqual(a, b, state); + } + if (constructor === Set) { + return areSetsEqual(a, b, state); + } + // Since this is a custom object, capture the string tag to determing its type. + // This is reasonably performant in modern environments like v8 and SpiderMonkey. + const tag = getTag(a); + if (tag === DATE_TAG) { + return areDatesEqual(a, b, state); + } + if (tag === REG_EXP_TAG) { + return areRegExpsEqual(a, b, state); + } + if (tag === MAP_TAG) { + return areMapsEqual(a, b, state); + } + if (tag === SET_TAG) { + return areSetsEqual(a, b, state); + } + if (tag === OBJECT_TAG) { + // The exception for value comparison is custom `Promise`-like class instances. These should + // be treated the same as standard `Promise` objects, which means strict equality, and if + // it reaches this point then that strict equality comparison has already failed. + return (typeof a.then !== "function" && + typeof b.then !== "function" && + areObjectsEqual(a, b, state)); + } + // If an arguments tag, it should be treated as a standard object. + if (tag === ARGUMENTS_TAG) { + return areObjectsEqual(a, b, state); + } + // As the penultimate fallback, check if the values passed are primitive wrappers. This + // is very rare in modern JS, which is why it is deprioritized compared to all other object + // types. + if (tag === BOOLEAN_TAG || tag === NUMBER_TAG || tag === STRING_TAG) { + return arePrimitiveWrappersEqual(a, b, state); + } + // If not matching any tags that require a specific type of comparison, then we hard-code false because + // the only thing remaining is strict equality, which has already been compared. This is for a few reasons: + // - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only + // comparison that can be made. + // - For types that can be introspected, but rarely have requirements to be compared + // (`ArrayBuffer`, `DataView`, etc.), the cost is avoided to prioritize the common + // use-cases (may be included in a future release, if requested enough). + // - For types that can be introspected but do not have an objective definition of what + // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare. + // In all cases, these decisions should be reevaluated based on changes to the language and + // common development practices. + return false; + }; +} +/** + * Create the configuration object used for building comparators. + */ +export function createEqualityComparatorConfig({ circular, createCustomConfig, strict, }) { + let config = { + areArraysEqual: strict + ? areObjectsEqualStrictDefault + : areArraysEqualDefault, + areDatesEqual: areDatesEqualDefault, + areMapsEqual: strict + ? combineComparators(areMapsEqualDefault, areObjectsEqualStrictDefault) + : areMapsEqualDefault, + areObjectsEqual: strict + ? areObjectsEqualStrictDefault + : areObjectsEqualDefault, + arePrimitiveWrappersEqual: arePrimitiveWrappersEqualDefault, + areRegExpsEqual: areRegExpsEqualDefault, + areSetsEqual: strict + ? combineComparators(areSetsEqualDefault, areObjectsEqualStrictDefault) + : areSetsEqualDefault, + areTypedArraysEqual: strict + ? areObjectsEqualStrictDefault + : areTypedArraysEqual, + }; + if (createCustomConfig) { + config = assign({}, config, createCustomConfig(config)); + } + if (circular) { + const areArraysEqual = createIsCircular(config.areArraysEqual); + const areMapsEqual = createIsCircular(config.areMapsEqual); + const areObjectsEqual = createIsCircular(config.areObjectsEqual); + const areSetsEqual = createIsCircular(config.areSetsEqual); + config = assign({}, config, { + areArraysEqual, + areMapsEqual, + areObjectsEqual, + areSetsEqual, + }); + } + return config; +} +/** + * Default equality comparator pass-through, used as the standard `isEqual` creator for + * use inside the built comparator. + */ +export function createInternalEqualityComparator(compare) { + return function (a, b, _indexOrKeyA, _indexOrKeyB, _parentA, _parentB, state) { + return compare(a, b, state); + }; +} +/** + * Create the `isEqual` function used by the consuming application. + */ +export function createIsEqual({ circular, comparator, createState, equals, strict, }) { + if (createState) { + return function isEqual(a, b) { + const { cache = circular ? new WeakMap() : undefined, meta } = createState(); + return comparator(a, b, { + cache, + equals, + meta, + strict, + }); + }; + } + if (circular) { + return function isEqual(a, b) { + return comparator(a, b, { + cache: new WeakMap(), + equals, + meta: undefined, + strict, + }); + }; + } + const state = { + cache: undefined, + equals, + meta: undefined, + strict, + }; + return function isEqual(a, b) { + return comparator(a, b, state); + }; +} diff --git a/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/equals.js b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/equals.js new file mode 100644 index 0000000000..ab7b4d6270 --- /dev/null +++ b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/equals.js @@ -0,0 +1,201 @@ +// DO NOT MODIFY +// +// Generated with scripts/sdk_actor/compile_bridge.ts + +import { getStrictProperties, hasOwn, sameValueZeroEqual } from "./utils.js"; +const OWNER = "_owner"; +const { getOwnPropertyDescriptor, keys } = Object; +/** + * Whether the arrays are equal in value. + */ +export function areArraysEqual(a, b, state) { + let index = a.length; + if (b.length !== index) { + return false; + } + while (index-- > 0) { + if (!state.equals(a[index], b[index], index, index, a, b, state)) { + return false; + } + } + return true; +} +/** + * Whether the dates passed are equal in value. + */ +export function areDatesEqual(a, b) { + return sameValueZeroEqual(a.getTime(), b.getTime()); +} +/** + * Whether the `Map`s are equal in value. + */ +export function areMapsEqual(a, b, state) { + if (a.size !== b.size) { + return false; + } + const matchedIndices = {}; + const aIterable = a.entries(); + let index = 0; + let aResult; + let bResult; + while ((aResult = aIterable.next())) { + if (aResult.done) { + break; + } + const bIterable = b.entries(); + let hasMatch = false; + let matchIndex = 0; + while ((bResult = bIterable.next())) { + if (bResult.done) { + break; + } + const [aKey, aValue] = aResult.value; + const [bKey, bValue] = bResult.value; + if (!hasMatch && + !matchedIndices[matchIndex] && + (hasMatch = + state.equals(aKey, bKey, index, matchIndex, a, b, state) && + state.equals(aValue, bValue, aKey, bKey, a, b, state))) { + matchedIndices[matchIndex] = true; + } + matchIndex++; + } + if (!hasMatch) { + return false; + } + index++; + } + return true; +} +/** + * Whether the objects are equal in value. + */ +export function areObjectsEqual(a, b, state) { + const properties = keys(a); + let index = properties.length; + if (keys(b).length !== index) { + return false; + } + let property; + // Decrementing `while` showed faster results than either incrementing or + // decrementing `for` loop and than an incrementing `while` loop. Declarative + // methods like `some` / `every` were not used to avoid incurring the garbage + // cost of anonymous callbacks. + while (index-- > 0) { + property = properties[index]; + if (property === OWNER && + (a.$$typeof || b.$$typeof) && + a.$$typeof !== b.$$typeof) { + return false; + } + if (!hasOwn(b, property) || + !state.equals(a[property], b[property], property, property, a, b, state)) { + return false; + } + } + return true; +} +/** + * Whether the objects are equal in value with strict property checking. + */ +export function areObjectsEqualStrict(a, b, state) { + const properties = getStrictProperties(a); + let index = properties.length; + if (getStrictProperties(b).length !== index) { + return false; + } + let property; + let descriptorA; + let descriptorB; + // Decrementing `while` showed faster results than either incrementing or + // decrementing `for` loop and than an incrementing `while` loop. Declarative + // methods like `some` / `every` were not used to avoid incurring the garbage + // cost of anonymous callbacks. + while (index-- > 0) { + property = properties[index]; + if (property === OWNER && + (a.$$typeof || b.$$typeof) && + a.$$typeof !== b.$$typeof) { + return false; + } + if (!hasOwn(b, property)) { + return false; + } + if (!state.equals(a[property], b[property], property, property, a, b, state)) { + return false; + } + descriptorA = getOwnPropertyDescriptor(a, property); + descriptorB = getOwnPropertyDescriptor(b, property); + if ((descriptorA || descriptorB) && + (!descriptorA || + !descriptorB || + descriptorA.configurable !== descriptorB.configurable || + descriptorA.enumerable !== descriptorB.enumerable || + descriptorA.writable !== descriptorB.writable)) { + return false; + } + } + return true; +} +/** + * Whether the primitive wrappers passed are equal in value. + */ +export function arePrimitiveWrappersEqual(a, b) { + return sameValueZeroEqual(a.valueOf(), b.valueOf()); +} +/** + * Whether the regexps passed are equal in value. + */ +export function areRegExpsEqual(a, b) { + return a.source === b.source && a.flags === b.flags; +} +/** + * Whether the `Set`s are equal in value. + */ +export function areSetsEqual(a, b, state) { + if (a.size !== b.size) { + return false; + } + const matchedIndices = {}; + const aIterable = a.values(); + let aResult; + let bResult; + while ((aResult = aIterable.next())) { + if (aResult.done) { + break; + } + const bIterable = b.values(); + let hasMatch = false; + let matchIndex = 0; + while ((bResult = bIterable.next())) { + if (bResult.done) { + break; + } + if (!hasMatch && + !matchedIndices[matchIndex] && + (hasMatch = state.equals(aResult.value, bResult.value, aResult.value, bResult.value, a, b, state))) { + matchedIndices[matchIndex] = true; + } + matchIndex++; + } + if (!hasMatch) { + return false; + } + } + return true; +} +/** + * Whether the TypedArray instances are equal in value. + */ +export function areTypedArraysEqual(a, b) { + let index = a.length; + if (b.length !== index) { + return false; + } + while (index-- > 0) { + if (a[index] !== b[index]) { + return false; + } + } + return true; +} diff --git a/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/index.js b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/index.js new file mode 100644 index 0000000000..fe53244fa3 --- /dev/null +++ b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/index.js @@ -0,0 +1,74 @@ +// DO NOT MODIFY +// +// Generated with scripts/sdk_actor/compile_bridge.ts + +import { createEqualityComparatorConfig, createEqualityComparator, createInternalEqualityComparator, createIsEqual, } from "./comparator.js"; +import { sameValueZeroEqual } from "./utils.js"; +export { sameValueZeroEqual }; +export * from "./internalTypes.js"; +/** + * Whether the items passed are deeply-equal in value. + */ +export const deepEqual = createCustomEqual(); +/** + * Whether the items passed are deeply-equal in value based on strict comparison. + */ +export const strictDeepEqual = createCustomEqual({ strict: true }); +/** + * Whether the items passed are deeply-equal in value, including circular references. + */ +export const circularDeepEqual = createCustomEqual({ circular: true }); +/** + * Whether the items passed are deeply-equal in value, including circular references, + * based on strict comparison. + */ +export const strictCircularDeepEqual = createCustomEqual({ + circular: true, + strict: true, +}); +/** + * Whether the items passed are shallowly-equal in value. + */ +export const shallowEqual = createCustomEqual({ + createInternalComparator: () => sameValueZeroEqual, +}); +/** + * Whether the items passed are shallowly-equal in value based on strict comparison + */ +export const strictShallowEqual = createCustomEqual({ + strict: true, + createInternalComparator: () => sameValueZeroEqual, +}); +/** + * Whether the items passed are shallowly-equal in value, including circular references. + */ +export const circularShallowEqual = createCustomEqual({ + circular: true, + createInternalComparator: () => sameValueZeroEqual, +}); +/** + * Whether the items passed are shallowly-equal in value, including circular references, + * based on strict comparison. + */ +export const strictCircularShallowEqual = createCustomEqual({ + circular: true, + createInternalComparator: () => sameValueZeroEqual, + strict: true, +}); +/** + * Create a custom equality comparison method. + * + * This can be done to create very targeted comparisons in extreme hot-path scenarios + * where the standard methods are not performant enough, but can also be used to provide + * support for legacy environments that do not support expected features like + * `RegExp.prototype.flags` out of the box. + */ +export function createCustomEqual(options = {}) { + const { circular = false, createInternalComparator: createCustomInternalComparator, createState, strict = false, } = options; + const config = createEqualityComparatorConfig(options); + const comparator = createEqualityComparator(config); + const equals = createCustomInternalComparator + ? createCustomInternalComparator(comparator) + : createInternalEqualityComparator(comparator); + return createIsEqual({ circular, comparator, createState, equals, strict }); +} diff --git a/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/internalTypes.js b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/internalTypes.js new file mode 100644 index 0000000000..25eac70081 --- /dev/null +++ b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/internalTypes.js @@ -0,0 +1,5 @@ +// DO NOT MODIFY +// +// Generated with scripts/sdk_actor/compile_bridge.ts + +export {}; diff --git a/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/utils.js b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/utils.js new file mode 100644 index 0000000000..52c753f13e --- /dev/null +++ b/packages/infra/client/isolate-v8-runner/js/lib/fast-equals/utils.js @@ -0,0 +1,56 @@ +// DO NOT MODIFY +// +// Generated with scripts/sdk_actor/compile_bridge.ts + +const { getOwnPropertyNames, getOwnPropertySymbols } = Object; +const { hasOwnProperty } = Object.prototype; +/** + * Combine two comparators into a single comparators. + */ +export function combineComparators(comparatorA, comparatorB) { + return function isEqual(a, b, state) { + return comparatorA(a, b, state) && comparatorB(a, b, state); + }; +} +/** + * Wrap the provided `areItemsEqual` method to manage the circular state, allowing + * for circular references to be safely included in the comparison without creating + * stack overflows. + */ +export function createIsCircular(areItemsEqual) { + return function isCircular(a, b, state) { + if (!a || !b || typeof a !== "object" || typeof b !== "object") { + return areItemsEqual(a, b, state); + } + const { cache } = state; + const cachedA = cache.get(a); + const cachedB = cache.get(b); + if (cachedA && cachedB) { + return cachedA === b && cachedB === a; + } + cache.set(a, b); + cache.set(b, a); + const result = areItemsEqual(a, b, state); + cache.delete(a); + cache.delete(b); + return result; + }; +} +/** + * Get the properties to strictly examine, which include both own properties that are + * not enumerable and symbol properties. + */ +export function getStrictProperties(object) { + return getOwnPropertyNames(object).concat(getOwnPropertySymbols(object)); +} +/** + * Whether the object contains the property passed as an own property. + */ +export const hasOwn = Object.hasOwn || + ((object, property) => hasOwnProperty.call(object, property)); +/** + * Whether the values passed are strictly equal or both NaN. + */ +export function sameValueZeroEqual(a, b) { + return a || b ? a === b : a === b || (a !== a && b !== b); +} diff --git a/packages/infra/client/isolate-v8-runner/src/ext/kv.rs b/packages/infra/client/isolate-v8-runner/src/ext/kv.rs index 225cd4f3d6..7a424178f5 100644 --- a/packages/infra/client/isolate-v8-runner/src/ext/kv.rs +++ b/packages/infra/client/isolate-v8-runner/src/ext/kv.rs @@ -19,6 +19,13 @@ deno_core::extension!( ], esm = [ dir "js", + // Order matters + "lib/fast-equals/utils.js", + "lib/fast-equals/equals.js", + "lib/fast-equals/comparator.js", + "lib/fast-equals/internalTypes.js", + "lib/fast-equals/index.js", + "40_rivet_kv.js", ], options = { diff --git a/packages/infra/client/isolate-v8-runner/src/log_shipper.rs b/packages/infra/client/isolate-v8-runner/src/log_shipper.rs index 1b529f6e45..ffe80ff072 100644 --- a/packages/infra/client/isolate-v8-runner/src/log_shipper.rs +++ b/packages/infra/client/isolate-v8-runner/src/log_shipper.rs @@ -171,7 +171,7 @@ pub fn ship_logs( let mut throttle_error = throttle::Throttle::new(1, Duration::from_secs(60)); // How many lines have been logged as a preview, see `MAX_PREVIEW_LINES` - let mut preview_iine_count = 0; + let mut preview_line_count = 0; for line in stream.lines() { // Throttle @@ -216,8 +216,8 @@ pub fn ship_logs( } // Log preview of lines from the program for easy debugging from Pegboard - if preview_iine_count < MAX_PREVIEW_LINES { - preview_iine_count += 1; + if preview_line_count < MAX_PREVIEW_LINES { + preview_line_count += 1; tracing::info!( ?actor_id, "{stream_type:?}: {message}", @@ -225,7 +225,7 @@ pub fn ship_logs( message = message, ); - if preview_iine_count == MAX_PREVIEW_LINES { + if preview_line_count == MAX_PREVIEW_LINES { tracing::warn!( ?actor_id, "{stream_type:?}: ...not logging any more lines...", diff --git a/packages/infra/client/isolate-v8-runner/tests/index.js b/packages/infra/client/isolate-v8-runner/tests/index.js index faad1db2bf..77f4d81060 100644 --- a/packages/infra/client/isolate-v8-runner/tests/index.js +++ b/packages/infra/client/isolate-v8-runner/tests/index.js @@ -4,10 +4,12 @@ export default { async start(ctx) { console.log(ctx); - await ctx.kv.put(['foob', 'b'], 1); + await ctx.kv.putBatch(new Map([[['foob', 'b'], 12], [['foob', 'a'], null], [['foob', 'c'], true]])); - let res = await ctx.kv.getBatch(['foob', 'b']); - console.log(res, res.get(['foob', 'b'])); + let res = await ctx.kv.list({ prefix: ['foob'] }); + + console.log(res.array(), res.raw(), res.entries()); + console.log(res.get(['foob', 'b'])); Deno.exit(2); diff --git a/scripts/sdk_actor/compile_bridge.ts b/scripts/sdk_actor/compile_bridge.ts index 101b1b5dbb..1c99541691 100755 --- a/scripts/sdk_actor/compile_bridge.ts +++ b/scripts/sdk_actor/compile_bridge.ts @@ -15,14 +15,13 @@ const ISOLATE_RUNNER_JS_PATH = const ACTOR_CORE_BRIDGE_TYPES_PATH = resolve(ACTOR_SDK_PATH, "core", "src", "bridge_types"); // Clean folders -await Deno.remove(ACTOR_BRIDGE_TYPES_PATH, { recursive: true }).catch(() => {}); -await Deno.remove(ACTOR_CORE_BRIDGE_TYPES_PATH, { recursive: true }).catch(() => {}); -await Deno.remove(ISOLATE_RUNNER_JS_PATH, { recursive: true }).catch(() => {}); +await Deno.remove(ACTOR_BRIDGE_TYPES_PATH, { recursive: true }).catch(() => { }); +await Deno.remove(ACTOR_CORE_BRIDGE_TYPES_PATH, { recursive: true }).catch(() => { }); +await Deno.remove(ISOLATE_RUNNER_JS_PATH, { recursive: true }).catch(() => { }); // Compile JS bridge await $`npx -p typescript@5.7.2 tsc -p tsconfig.bridge.json`.cwd(ACTOR_BRIDGE_PATH); -// Add header to JS bridge for await (const entry of walk( ISOLATE_RUNNER_JS_PATH, { @@ -30,7 +29,9 @@ for await (const entry of walk( includeDirs: false, } )) { - const content = await Deno.readTextFile(entry.path); + let content = await Deno.readTextFile(entry.path); + + // Add header to JS bridge await Deno.writeTextFile( entry.path, "// DO NOT MODIFY\n//\n// Generated with scripts/sdk_actor/compile_bridge.ts\n\n" + content diff --git a/sdks/actor/biome.json b/sdks/actor/biome.json index ccf8dfb542..4f5cbb16d3 100644 --- a/sdks/actor/biome.json +++ b/sdks/actor/biome.json @@ -6,5 +6,10 @@ "noUselessElse": "off" } } + }, + "files": { + "ignore": [ + "bridge/types/lib/fast-equals" + ] } } diff --git a/sdks/actor/bridge/src/bridge/40_rivet_kv.ts b/sdks/actor/bridge/src/bridge/40_rivet_kv.ts index 309e318eca..ca1923ece4 100644 --- a/sdks/actor/bridge/src/bridge/40_rivet_kv.ts +++ b/sdks/actor/bridge/src/bridge/40_rivet_kv.ts @@ -9,7 +9,9 @@ import { op_rivet_kv_put, op_rivet_kv_put_batch, } from "ext:core/ops"; -import type { InKey, ListQuery, OutKey } from "./types/metadata.d.ts"; +import type { InKey, ListQuery, OutEntry, OutKey } from "./types/metadata.d.ts"; + +import { deepEqual } from "./lib/fast-equals/index.js"; /** * Options for the `get` function. @@ -41,20 +43,13 @@ export interface GetBatchOptions { async function getBatch, V>( keys: K, options?: GetBatchOptions, -): Promise> { - const entries = await op_rivet_kv_get_batch(keys.map((x) => serializeKey(x))); - - const deserializedValues = new Map(); - - for (const [key, entry] of entries) { - const jsKey = deserializeKey(key) as K[number]; - deserializedValues.set( - jsKey, - deserializeValue(jsKey, entry.value, options?.format) as V, - ); - } +): Promise> { + const entries: [OutKey, OutEntry][] = await op_rivet_kv_get_batch(keys.map((x) => serializeKey(x))); - return deserializedValues; + return new HashMap(entries.map(([key, entry]) => { + let jsKey = deserializeKey(key) as K[number]; + return [jsKey, deserializeValue(jsKey, entry.value, options?.format) as V]; + })); } /** @@ -81,9 +76,9 @@ export interface ListOptions { * is used for filtering. * * @param {ListOptions} [options] - Options. - * @returns {Promise>} The retrieved values. + * @returns {Promise>} The retrieved values. */ -async function list(options?: ListOptions): Promise> { +async function list(options?: ListOptions): Promise> { // Build query let query: ListQuery; if (options?.prefix) { @@ -120,22 +115,16 @@ async function list(options?: ListOptions): Promise> { query = { all: {} }; } - const entries = await op_rivet_kv_list( + const entries: [OutKey, OutEntry][] = await op_rivet_kv_list( query, options?.reverse ?? false, options?.limit, ); - const deserializedValues = new Map(); - - for (const [key, entry] of entries) { - const jsKey = deserializeKey(key) as K; - deserializedValues.set( - jsKey, - deserializeValue(jsKey, entry.value, options?.format) as V, - ); - } - return deserializedValues; + return new HashMap(entries.map(([key, entry]) => { + let jsKey = deserializeKey(key) as K; + return [jsKey, deserializeValue(jsKey, entry.value, options?.format) as V]; + })); } /** @@ -344,6 +333,42 @@ function deserializeValue( } } +class HashMap { + #internal: [K, V][]; + + constructor(internal: [K, V][]) { + this.#internal = internal; + } + + get(key: K): V | undefined { + for (let [k, v] of this.#internal) { + if (deepEqual(key, k)) return v; + } + + return undefined; + } + + /** + * Returns a map of keys to values. **WARNING** Using `.get` on the returned map does not work as expected + * with complex types (arrays, objects, etc). Use `.get` on this class instead. + */ + raw() { + return new Map(this.#internal); + } + + array() { + return this.#internal; + } + + entries() { + return this[Symbol.iterator](); + } + + [Symbol.iterator]() { + return this.#internal[Symbol.iterator](); + } +} + export const KV_NAMESPACE = { get, getBatch, diff --git a/sdks/actor/bridge/src/bridge/lib/fast-equals/README.md b/sdks/actor/bridge/src/bridge/lib/fast-equals/README.md new file mode 100644 index 0000000000..35ff4d5290 --- /dev/null +++ b/sdks/actor/bridge/src/bridge/lib/fast-equals/README.md @@ -0,0 +1 @@ +Copied from https://github.com/planttheidea/fast-equals diff --git a/sdks/actor/bridge/src/bridge/lib/fast-equals/comparator.ts b/sdks/actor/bridge/src/bridge/lib/fast-equals/comparator.ts new file mode 100644 index 0000000000..bfc856b23d --- /dev/null +++ b/sdks/actor/bridge/src/bridge/lib/fast-equals/comparator.ts @@ -0,0 +1,316 @@ +import { + areArraysEqual as areArraysEqualDefault, + areDatesEqual as areDatesEqualDefault, + areMapsEqual as areMapsEqualDefault, + areObjectsEqual as areObjectsEqualDefault, + areObjectsEqualStrict as areObjectsEqualStrictDefault, + arePrimitiveWrappersEqual as arePrimitiveWrappersEqualDefault, + areRegExpsEqual as areRegExpsEqualDefault, + areSetsEqual as areSetsEqualDefault, + areTypedArraysEqual, +} from "./equals.js"; +import { combineComparators, createIsCircular } from "./utils.js"; +import type { + ComparatorConfig, + CreateState, + CustomEqualCreatorOptions, + EqualityComparator, + InternalEqualityComparator, + State, +} from "./internalTypes.js"; + +const ARGUMENTS_TAG = "[object Arguments]"; +const BOOLEAN_TAG = "[object Boolean]"; +const DATE_TAG = "[object Date]"; +const MAP_TAG = "[object Map]"; +const NUMBER_TAG = "[object Number]"; +const OBJECT_TAG = "[object Object]"; +const REG_EXP_TAG = "[object RegExp]"; +const SET_TAG = "[object Set]"; +const STRING_TAG = "[object String]"; + +const { isArray } = Array; +const isTypedArray = + typeof ArrayBuffer === "function" && ArrayBuffer.isView + ? ArrayBuffer.isView + : null; +const { assign } = Object; +const getTag = Object.prototype.toString.call.bind( + Object.prototype.toString, +) as (a: object) => string; + +interface CreateIsEqualOptions { + circular: boolean; + comparator: EqualityComparator; + createState: CreateState | undefined; + equals: InternalEqualityComparator; + strict: boolean; +} + +/** + * Create a comparator method based on the type-specific equality comparators passed. + */ +export function createEqualityComparator({ + areArraysEqual, + areDatesEqual, + areMapsEqual, + areObjectsEqual, + arePrimitiveWrappersEqual, + areRegExpsEqual, + areSetsEqual, + areTypedArraysEqual, +}: ComparatorConfig): EqualityComparator { + /** + * compare the value of the two objects and return true if they are equivalent in values + */ + return function comparator(a: any, b: any, state: State): boolean { + // If the items are strictly equal, no need to do a value comparison. + if (a === b) { + return true; + } + + // If the items are not non-nullish objects, then the only possibility + // of them being equal but not strictly is if they are both `NaN`. Since + // `NaN` is uniquely not equal to itself, we can use self-comparison of + // both objects, which is faster than `isNaN()`. + if ( + a == null || + b == null || + typeof a !== "object" || + typeof b !== "object" + ) { + return a !== a && b !== b; + } + + const constructor = a.constructor; + + // Checks are listed in order of commonality of use-case: + // 1. Common complex object types (plain object, array) + // 2. Common data values (date, regexp) + // 3. Less-common complex object types (map, set) + // 4. Less-common data values (promise, primitive wrappers) + // Inherently this is both subjective and assumptive, however + // when reviewing comparable libraries in the wild this order + // appears to be generally consistent. + + // Constructors should match, otherwise there is potential for false positives + // between class and subclass or custom object and POJO. + if (constructor !== b.constructor) { + return false; + } + + // `isPlainObject` only checks against the object"s own realm. Cross-realm + // comparisons are rare, and will be handled in the ultimate fallback, so + // we can avoid capturing the string tag. + if (constructor === Object) { + return areObjectsEqual(a, b, state); + } + + // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing + // the string tag or doing an `instanceof` check. + if (isArray(a)) { + return areArraysEqual(a, b, state); + } + + // `isTypedArray()` works on all possible TypedArray classes, so we can avoid + // capturing the string tag or comparing against all possible constructors. + if (isTypedArray != null && isTypedArray(a)) { + return areTypedArraysEqual(a, b, state); + } + + // Try to fast-path equality checks for other complex object types in the + // same realm to avoid capturing the string tag. Strict equality is used + // instead of `instanceof` because it is more performant for the common + // use-case. If someone is subclassing a native class, it will be handled + // with the string tag comparison. + + if (constructor === Date) { + return areDatesEqual(a, b, state); + } + + if (constructor === RegExp) { + return areRegExpsEqual(a, b, state); + } + + if (constructor === Map) { + return areMapsEqual(a, b, state); + } + + if (constructor === Set) { + return areSetsEqual(a, b, state); + } + + // Since this is a custom object, capture the string tag to determing its type. + // This is reasonably performant in modern environments like v8 and SpiderMonkey. + const tag = getTag(a); + + if (tag === DATE_TAG) { + return areDatesEqual(a, b, state); + } + + if (tag === REG_EXP_TAG) { + return areRegExpsEqual(a, b, state); + } + + if (tag === MAP_TAG) { + return areMapsEqual(a, b, state); + } + + if (tag === SET_TAG) { + return areSetsEqual(a, b, state); + } + + if (tag === OBJECT_TAG) { + // The exception for value comparison is custom `Promise`-like class instances. These should + // be treated the same as standard `Promise` objects, which means strict equality, and if + // it reaches this point then that strict equality comparison has already failed. + return ( + typeof a.then !== "function" && + typeof b.then !== "function" && + areObjectsEqual(a, b, state) + ); + } + + // If an arguments tag, it should be treated as a standard object. + if (tag === ARGUMENTS_TAG) { + return areObjectsEqual(a, b, state); + } + + // As the penultimate fallback, check if the values passed are primitive wrappers. This + // is very rare in modern JS, which is why it is deprioritized compared to all other object + // types. + if (tag === BOOLEAN_TAG || tag === NUMBER_TAG || tag === STRING_TAG) { + return arePrimitiveWrappersEqual(a, b, state); + } + + // If not matching any tags that require a specific type of comparison, then we hard-code false because + // the only thing remaining is strict equality, which has already been compared. This is for a few reasons: + // - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only + // comparison that can be made. + // - For types that can be introspected, but rarely have requirements to be compared + // (`ArrayBuffer`, `DataView`, etc.), the cost is avoided to prioritize the common + // use-cases (may be included in a future release, if requested enough). + // - For types that can be introspected but do not have an objective definition of what + // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare. + // In all cases, these decisions should be reevaluated based on changes to the language and + // common development practices. + return false; + }; +} + +/** + * Create the configuration object used for building comparators. + */ +export function createEqualityComparatorConfig({ + circular, + createCustomConfig, + strict, +}: CustomEqualCreatorOptions): ComparatorConfig { + let config = { + areArraysEqual: strict + ? areObjectsEqualStrictDefault + : areArraysEqualDefault, + areDatesEqual: areDatesEqualDefault, + areMapsEqual: strict + ? combineComparators(areMapsEqualDefault, areObjectsEqualStrictDefault) + : areMapsEqualDefault, + areObjectsEqual: strict + ? areObjectsEqualStrictDefault + : areObjectsEqualDefault, + arePrimitiveWrappersEqual: arePrimitiveWrappersEqualDefault, + areRegExpsEqual: areRegExpsEqualDefault, + areSetsEqual: strict + ? combineComparators(areSetsEqualDefault, areObjectsEqualStrictDefault) + : areSetsEqualDefault, + areTypedArraysEqual: strict + ? areObjectsEqualStrictDefault + : areTypedArraysEqual, + }; + + if (createCustomConfig) { + config = assign({}, config, createCustomConfig(config)); + } + + if (circular) { + const areArraysEqual = createIsCircular(config.areArraysEqual); + const areMapsEqual = createIsCircular(config.areMapsEqual); + const areObjectsEqual = createIsCircular(config.areObjectsEqual); + const areSetsEqual = createIsCircular(config.areSetsEqual); + + config = assign({}, config, { + areArraysEqual, + areMapsEqual, + areObjectsEqual, + areSetsEqual, + }); + } + + return config; +} + +/** + * Default equality comparator pass-through, used as the standard `isEqual` creator for + * use inside the built comparator. + */ +export function createInternalEqualityComparator( + compare: EqualityComparator, +): InternalEqualityComparator { + return function ( + a: any, + b: any, + _indexOrKeyA: any, + _indexOrKeyB: any, + _parentA: any, + _parentB: any, + state: State, + ) { + return compare(a, b, state); + }; +} + +/** + * Create the `isEqual` function used by the consuming application. + */ +export function createIsEqual({ + circular, + comparator, + createState, + equals, + strict, +}: CreateIsEqualOptions) { + if (createState) { + return function isEqual(a: A, b: B): boolean { + const { cache = circular ? new WeakMap() : undefined, meta } = + createState!(); + + return comparator(a, b, { + cache, + equals, + meta, + strict, + } as State); + }; + } + + if (circular) { + return function isEqual(a: A, b: B): boolean { + return comparator(a, b, { + cache: new WeakMap(), + equals, + meta: undefined as Meta, + strict, + } as State); + }; + } + + const state = { + cache: undefined, + equals, + meta: undefined, + strict, + } as State; + + return function isEqual(a: A, b: B): boolean { + return comparator(a, b, state); + }; +} diff --git a/sdks/actor/bridge/src/bridge/lib/fast-equals/equals.ts b/sdks/actor/bridge/src/bridge/lib/fast-equals/equals.ts new file mode 100644 index 0000000000..4faf4b341b --- /dev/null +++ b/sdks/actor/bridge/src/bridge/lib/fast-equals/equals.ts @@ -0,0 +1,300 @@ +import { getStrictProperties, hasOwn, sameValueZeroEqual } from "./utils.js"; +import type { + Dictionary, + PrimitiveWrapper, + State, + TypedArray, +} from "./internalTypes.js"; + +const OWNER = "_owner"; + +const { getOwnPropertyDescriptor, keys } = Object; + +/** + * Whether the arrays are equal in value. + */ +export function areArraysEqual(a: any[], b: any[], state: State) { + let index = a.length; + + if (b.length !== index) { + return false; + } + + while (index-- > 0) { + if (!state.equals(a[index], b[index], index, index, a, b, state)) { + return false; + } + } + + return true; +} + +/** + * Whether the dates passed are equal in value. + */ +export function areDatesEqual(a: Date, b: Date): boolean { + return sameValueZeroEqual(a.getTime(), b.getTime()); +} + +/** + * Whether the `Map`s are equal in value. + */ +export function areMapsEqual( + a: Map, + b: Map, + state: State, +): boolean { + if (a.size !== b.size) { + return false; + } + + const matchedIndices: Record = {}; + const aIterable = a.entries(); + + let index = 0; + let aResult: IteratorResult<[any, any]>; + let bResult: IteratorResult<[any, any]>; + + while ((aResult = aIterable.next())) { + if (aResult.done) { + break; + } + + const bIterable = b.entries(); + + let hasMatch = false; + let matchIndex = 0; + + while ((bResult = bIterable.next())) { + if (bResult.done) { + break; + } + + const [aKey, aValue] = aResult.value; + const [bKey, bValue] = bResult.value; + + if ( + !hasMatch && + !matchedIndices[matchIndex] && + (hasMatch = + state.equals(aKey, bKey, index, matchIndex, a, b, state) && + state.equals(aValue, bValue, aKey, bKey, a, b, state)) + ) { + matchedIndices[matchIndex] = true; + } + + matchIndex++; + } + + if (!hasMatch) { + return false; + } + + index++; + } + + return true; +} + +/** + * Whether the objects are equal in value. + */ +export function areObjectsEqual( + a: Dictionary, + b: Dictionary, + state: State, +): boolean { + const properties = keys(a); + + let index = properties.length; + + if (keys(b).length !== index) { + return false; + } + + let property: string; + + // Decrementing `while` showed faster results than either incrementing or + // decrementing `for` loop and than an incrementing `while` loop. Declarative + // methods like `some` / `every` were not used to avoid incurring the garbage + // cost of anonymous callbacks. + while (index-- > 0) { + property = properties[index]!; + + if ( + property === OWNER && + (a.$$typeof || b.$$typeof) && + a.$$typeof !== b.$$typeof + ) { + return false; + } + + if ( + !hasOwn(b, property) || + !state.equals(a[property], b[property], property, property, a, b, state) + ) { + return false; + } + } + + return true; +} + +/** + * Whether the objects are equal in value with strict property checking. + */ +export function areObjectsEqualStrict( + a: Dictionary, + b: Dictionary, + state: State, +): boolean { + const properties = getStrictProperties(a); + + let index = properties.length; + + if (getStrictProperties(b).length !== index) { + return false; + } + + let property: string | symbol; + let descriptorA: ReturnType; + let descriptorB: ReturnType; + + // Decrementing `while` showed faster results than either incrementing or + // decrementing `for` loop and than an incrementing `while` loop. Declarative + // methods like `some` / `every` were not used to avoid incurring the garbage + // cost of anonymous callbacks. + while (index-- > 0) { + property = properties[index]!; + + if ( + property === OWNER && + (a.$$typeof || b.$$typeof) && + a.$$typeof !== b.$$typeof + ) { + return false; + } + + if (!hasOwn(b, property)) { + return false; + } + + if ( + !state.equals(a[property], b[property], property, property, a, b, state) + ) { + return false; + } + + descriptorA = getOwnPropertyDescriptor(a, property); + descriptorB = getOwnPropertyDescriptor(b, property); + + if ( + (descriptorA || descriptorB) && + (!descriptorA || + !descriptorB || + descriptorA.configurable !== descriptorB.configurable || + descriptorA.enumerable !== descriptorB.enumerable || + descriptorA.writable !== descriptorB.writable) + ) { + return false; + } + } + + return true; +} + +/** + * Whether the primitive wrappers passed are equal in value. + */ +export function arePrimitiveWrappersEqual( + a: PrimitiveWrapper, + b: PrimitiveWrapper, +): boolean { + return sameValueZeroEqual(a.valueOf(), b.valueOf()); +} + +/** + * Whether the regexps passed are equal in value. + */ +export function areRegExpsEqual(a: RegExp, b: RegExp): boolean { + return a.source === b.source && a.flags === b.flags; +} + +/** + * Whether the `Set`s are equal in value. + */ +export function areSetsEqual( + a: Set, + b: Set, + state: State, +): boolean { + if (a.size !== b.size) { + return false; + } + + const matchedIndices: Record = {}; + const aIterable = a.values(); + + let aResult: IteratorResult; + let bResult: IteratorResult; + + while ((aResult = aIterable.next())) { + if (aResult.done) { + break; + } + + const bIterable = b.values(); + + let hasMatch = false; + let matchIndex = 0; + + while ((bResult = bIterable.next())) { + if (bResult.done) { + break; + } + + if ( + !hasMatch && + !matchedIndices[matchIndex] && + (hasMatch = state.equals( + aResult.value, + bResult.value, + aResult.value, + bResult.value, + a, + b, + state, + )) + ) { + matchedIndices[matchIndex] = true; + } + + matchIndex++; + } + + if (!hasMatch) { + return false; + } + } + + return true; +} + +/** + * Whether the TypedArray instances are equal in value. + */ +export function areTypedArraysEqual(a: TypedArray, b: TypedArray) { + let index = a.length; + + if (b.length !== index) { + return false; + } + + while (index-- > 0) { + if (a[index] !== b[index]) { + return false; + } + } + + return true; +} diff --git a/sdks/actor/bridge/src/bridge/lib/fast-equals/index.ts b/sdks/actor/bridge/src/bridge/lib/fast-equals/index.ts new file mode 100644 index 0000000000..50ffe00491 --- /dev/null +++ b/sdks/actor/bridge/src/bridge/lib/fast-equals/index.ts @@ -0,0 +1,95 @@ +import { + createEqualityComparatorConfig, + createEqualityComparator, + createInternalEqualityComparator, + createIsEqual, +} from "./comparator.js"; +import type { CustomEqualCreatorOptions } from "./internalTypes.js"; +import { sameValueZeroEqual } from "./utils.js"; + +export { sameValueZeroEqual }; +export * from "./internalTypes.js"; + +/** + * Whether the items passed are deeply-equal in value. + */ +export const deepEqual = createCustomEqual(); + +/** + * Whether the items passed are deeply-equal in value based on strict comparison. + */ +export const strictDeepEqual = createCustomEqual({ strict: true }); + +/** + * Whether the items passed are deeply-equal in value, including circular references. + */ +export const circularDeepEqual = createCustomEqual({ circular: true }); + +/** + * Whether the items passed are deeply-equal in value, including circular references, + * based on strict comparison. + */ +export const strictCircularDeepEqual = createCustomEqual({ + circular: true, + strict: true, +}); + +/** + * Whether the items passed are shallowly-equal in value. + */ +export const shallowEqual = createCustomEqual({ + createInternalComparator: () => sameValueZeroEqual, +}); + +/** + * Whether the items passed are shallowly-equal in value based on strict comparison + */ +export const strictShallowEqual = createCustomEqual({ + strict: true, + createInternalComparator: () => sameValueZeroEqual, +}); + +/** + * Whether the items passed are shallowly-equal in value, including circular references. + */ +export const circularShallowEqual = createCustomEqual({ + circular: true, + createInternalComparator: () => sameValueZeroEqual, +}); + +/** + * Whether the items passed are shallowly-equal in value, including circular references, + * based on strict comparison. + */ +export const strictCircularShallowEqual = createCustomEqual({ + circular: true, + createInternalComparator: () => sameValueZeroEqual, + strict: true, +}); + +/** + * Create a custom equality comparison method. + * + * This can be done to create very targeted comparisons in extreme hot-path scenarios + * where the standard methods are not performant enough, but can also be used to provide + * support for legacy environments that do not support expected features like + * `RegExp.prototype.flags` out of the box. + */ +export function createCustomEqual( + options: CustomEqualCreatorOptions = {}, +) { + const { + circular = false, + createInternalComparator: createCustomInternalComparator, + createState, + strict = false, + } = options; + + const config = createEqualityComparatorConfig(options); + const comparator = createEqualityComparator(config); + const equals = createCustomInternalComparator + ? createCustomInternalComparator(comparator) + : createInternalEqualityComparator(comparator); + + return createIsEqual({ circular, comparator, createState, equals, strict }); +} diff --git a/sdks/actor/bridge/src/bridge/lib/fast-equals/internalTypes.ts b/sdks/actor/bridge/src/bridge/lib/fast-equals/internalTypes.ts new file mode 100644 index 0000000000..58e1c31abe --- /dev/null +++ b/sdks/actor/bridge/src/bridge/lib/fast-equals/internalTypes.ts @@ -0,0 +1,185 @@ +/** + * Cache used to store references to objects, used for circular + * reference checks. + */ +export interface Cache { + delete(key: Key): boolean; + get(key: Key): Value | undefined; + set(key: Key, value: any): any; +} + +export interface State { + /** + * Cache used to identify circular references + */ + readonly cache: Cache | undefined; + /** + * Method used to determine equality of nested value. + */ + readonly equals: InternalEqualityComparator; + /** + * Additional value that can be used for comparisons. + */ + meta: Meta; + /** + * Whether the equality comparison is strict, meaning it matches + * all properties (including symbols and non-enumerable properties) + * with equal shape of descriptors. + */ + readonly strict: boolean; +} + +export interface CircularState extends State { + readonly cache: Cache; +} + +export interface DefaultState extends State { + readonly cache: undefined; +} + +export interface Dictionary { + [key: string | symbol]: Value; + $$typeof?: any; +} + +export interface ComparatorConfig { + /** + * Whether the arrays passed are equal in value. In strict mode, this includes + * additional properties added to the array. + */ + areArraysEqual: TypeEqualityComparator; + /** + * Whether the dates passed are equal in value. + */ + areDatesEqual: TypeEqualityComparator; + /** + * Whether the maps passed are equal in value. In strict mode, this includes + * additional properties added to the map. + */ + areMapsEqual: TypeEqualityComparator; + /** + * Whether the objects passed are equal in value. In strict mode, this includes + * non-enumerable properties added to the map, as well as symbol properties. + */ + areObjectsEqual: TypeEqualityComparator; + /** + * Whether the primitive wrappers passed are equal in value. + */ + arePrimitiveWrappersEqual: TypeEqualityComparator; + /** + * Whether the regexps passed are equal in value. + */ + areRegExpsEqual: TypeEqualityComparator; + /** + * Whether the sets passed are equal in value. In strict mode, this includes + * additional properties added to the set. + */ + areSetsEqual: TypeEqualityComparator; + /** + * Whether the typed arrays passed are equal in value. In strict mode, this includes + * additional properties added to the typed array. + */ + areTypedArraysEqual: TypeEqualityComparator; +} + +export type CreateCustomComparatorConfig = ( + config: ComparatorConfig, +) => Partial>; + +export type CreateState = () => { + cache?: Cache | undefined; + meta?: Meta; +}; + +export type EqualityComparator = ( + a: A, + b: B, + state: State, +) => boolean; +export type AnyEqualityComparator = ( + a: any, + b: any, + state: State, +) => boolean; + +export type EqualityComparatorCreator = ( + fn: EqualityComparator, +) => InternalEqualityComparator; + +export type InternalEqualityComparator = ( + a: any, + b: any, + indexOrKeyA: any, + indexOrKeyB: any, + parentA: any, + parentB: any, + state: State, +) => boolean; + +// We explicitly check for primitive wrapper types +// eslint-disable-next-line @typescript-eslint/ban-types +export type PrimitiveWrapper = Boolean | Number | String; + +/** + * Type which encompasses possible instances of TypedArray + * classes. + * + * **NOTE**: This does not include `BigInt64Array` and + * `BitUint64Array` because those are part of ES2020 and + * not supported by certain TS configurations. If using + * either in `areTypedArraysEqual`, you can cast the + * instance as `TypedArray` and it will work as expected, + * because runtime checks will still work for those classes. + */ +export type TypedArray = + | Float32Array + | Float64Array + | Int8Array + | Int16Array + | Int32Array + | Uint16Array + | Uint32Array + | Uint8Array + | Uint8ClampedArray; + +export type TypeEqualityComparator = ( + a: Type, + b: Type, + state: State, +) => boolean; + +export interface CustomEqualCreatorOptions { + /** + * Whether circular references should be supported. It causes the + * comparison to be slower, but for objects that have circular references + * it is required to avoid stack overflows. + */ + circular?: boolean; + /** + * Create a custom configuration of type-specific equality comparators. + * This receives the default configuration, which allows either replacement + * or supersetting of the default methods. + */ + createCustomConfig?: CreateCustomComparatorConfig; + /** + * Create a custom internal comparator, which is used as an override to the + * default entry point for nested value equality comparisons. This is often + * used for doing custom logic for specific types (such as handling a specific + * class instance differently than other objects) or to incorporate `meta` in + * the comparison. See the recipes for examples. + */ + createInternalComparator?: ( + compare: EqualityComparator, + ) => InternalEqualityComparator; + /** + * Create a custom `state` object passed between the methods. This allows for + * custom `cache` and/or `meta` values to be used. + */ + createState?: CreateState; + /** + * Whether the equality comparison is strict, meaning it matches + * all properties (including symbols and non-enumerable properties) + * with equal shape of descriptors. + */ + strict?: boolean; +} diff --git a/sdks/actor/bridge/src/bridge/lib/fast-equals/utils.ts b/sdks/actor/bridge/src/bridge/lib/fast-equals/utils.ts new file mode 100644 index 0000000000..751fd93498 --- /dev/null +++ b/sdks/actor/bridge/src/bridge/lib/fast-equals/utils.ts @@ -0,0 +1,88 @@ +import { + AnyEqualityComparator, + Cache, + CircularState, + Dictionary, + State, + TypeEqualityComparator, +} from "./internalTypes.js"; + +const { getOwnPropertyNames, getOwnPropertySymbols } = Object; +const { hasOwnProperty } = Object.prototype; + +/** + * Combine two comparators into a single comparators. + */ +export function combineComparators( + comparatorA: AnyEqualityComparator, + comparatorB: AnyEqualityComparator, +) { + return function isEqual(a: A, b: B, state: State) { + return comparatorA(a, b, state) && comparatorB(a, b, state); + }; +} + +/** + * Wrap the provided `areItemsEqual` method to manage the circular state, allowing + * for circular references to be safely included in the comparison without creating + * stack overflows. + */ +export function createIsCircular< + AreItemsEqual extends TypeEqualityComparator, +>(areItemsEqual: AreItemsEqual): AreItemsEqual { + return function isCircular( + a: any, + b: any, + state: CircularState>, + ) { + if (!a || !b || typeof a !== "object" || typeof b !== "object") { + return areItemsEqual(a, b, state); + } + + const { cache } = state; + + const cachedA = cache.get(a); + const cachedB = cache.get(b); + + if (cachedA && cachedB) { + return cachedA === b && cachedB === a; + } + + cache.set(a, b); + cache.set(b, a); + + const result = areItemsEqual(a, b, state); + + cache.delete(a); + cache.delete(b); + + return result; + } as AreItemsEqual; +} + +/** + * Get the properties to strictly examine, which include both own properties that are + * not enumerable and symbol properties. + */ +export function getStrictProperties( + object: Dictionary, +): Array { + return (getOwnPropertyNames(object) as Array).concat( + getOwnPropertySymbols(object), + ); +} + +/** + * Whether the object contains the property passed as an own property. + */ +export const hasOwn = + Object.hasOwn || + ((object: Dictionary, property: number | string | symbol) => + hasOwnProperty.call(object, property)); + +/** + * Whether the values passed are strictly equal or both NaN. + */ +export function sameValueZeroEqual(a: any, b: any): boolean { + return a || b ? a === b : a === b || (a !== a && b !== b); +} diff --git a/sdks/actor/bridge/src/ext/ops.d.ts b/sdks/actor/bridge/src/ext/ops.d.ts index 61c3d49358..8721030ee6 100644 --- a/sdks/actor/bridge/src/ext/ops.d.ts +++ b/sdks/actor/bridge/src/ext/ops.d.ts @@ -8,12 +8,12 @@ declare module "ext:core/ops" { export function op_rivet_kv_get(key: InKey): Promise; export function op_rivet_kv_get_batch( keys: InKey[], - ): Promise>; + ): Promise>; export function op_rivet_kv_list( query: ListQuery, reverse: boolean, limit?: number, - ): Promise>; + ): Promise>; export function op_rivet_kv_put(key: InKey, value: Uint8Array): Promise; export function op_rivet_kv_put_batch( entries: Map, diff --git a/sdks/actor/bridge/tsconfig.json b/sdks/actor/bridge/tsconfig.json index 70f9877567..ed22eda7c8 100644 --- a/sdks/actor/bridge/tsconfig.json +++ b/sdks/actor/bridge/tsconfig.json @@ -8,8 +8,6 @@ "allowSyntheticDefaultImports": false, "paths": { "ext:rivet_kv/40_rivet_kv.js": ["./src/bridge/40_rivet_kv.ts"], - "ext:rivet_runtime/90_rivet_ns.js": ["./src/bridge/90_rivet_ns.ts"], - "ext:rivet_runtime/99_rivet_main.js": ["./src/bridge/90_rivet_main.ts"] } }, "include": ["src/**/*"] diff --git a/sdks/actor/bridge/types/40_rivet_kv.d.ts b/sdks/actor/bridge/types/40_rivet_kv.d.ts deleted file mode 100644 index 3144b8a240..0000000000 --- a/sdks/actor/bridge/types/40_rivet_kv.d.ts +++ /dev/null @@ -1,113 +0,0 @@ -// DO NOT MODIFY -// -// Generated from sdks/actor/bridge/ - -/** - * Options for the `get` function. - */ -export interface GetOptions { - format?: "value" | "arrayBuffer"; -} -/** - * Retrieves a value from the key-value store. - */ -declare function get(key: K, options?: GetOptions): Promise; -/** - * Options for the `getBatch` function. - */ -export interface GetBatchOptions { - format?: "value" | "arrayBuffer"; -} -/** - * Retrieves a batch of key-value pairs. - */ -declare function getBatch, V>( - keys: K, - options?: GetBatchOptions, -): Promise>; -/** - * Options for the `list` function. - */ -export interface ListOptions { - format?: "value" | "arrayBuffer"; - start?: K; - startAfter?: K; - end?: K; - prefix?: K; - reverse?: boolean; - limit?: number; -} -/** - * Retrieves all key-value pairs in the KV store. When using any of the options, the keys lexicographic order - * is used for filtering. - * - * @param {ListOptions} [options] - Options. - * @returns {Promise>} The retrieved values. - */ -declare function list(options?: ListOptions): Promise>; -/** - * Options for the `put` function. - */ -export interface PutOptions { - format?: "value" | "arrayBuffer"; -} -/** - * Stores a key-value pair in the key-value store. - * - * @param {Key} key - The key under which the value will be stored. - * @param {Entry | ArrayBuffer} value - The value to be stored, which will be serialized. - * @param {PutOptions} [options] - Options. - * @returns {Promise} A promise that resolves when the operation is complete. - */ -declare function put( - key: K, - value: V | ArrayBuffer, - options?: PutOptions, -): Promise; -/** - * Options for the `putBatch` function. - */ -export interface PutBatchOptions { - format?: "value" | "arrayBuffer"; -} -/** - * Stores a batch of key-value pairs. - * - * @param {Map} obj - An object containing key-value pairs to be stored. - * @param {PutBatchOptions} [options] - Options. - * @returns {Promise} A promise that resolves when the batch operation is complete. - */ -declare function putBatch( - obj: Map, - options?: PutBatchOptions, -): Promise; -/** - * Deletes a key-value pair from the key-value store. - * - * @param {Key} key - The key of the key-value pair to delete. - * @returns {Promise} A promise that resolves when the operation is complete. - */ -declare function delete_(key: K): Promise; -/** - * Deletes a batch of key-value pairs from the key-value store. - * - * @param {Key[]} keys - A list of keys to delete. - * @returns {Promise} A promise that resolves when the operation is complete. - */ -declare function deleteBatch>(keys: K): Promise; -/** - * Deletes all data from the key-value store. **This CANNOT be undone.** - * - * @returns {Promise} A promise that resolves when the operation is complete. - */ -declare function deleteAll(): Promise; -export declare const KV_NAMESPACE: { - get: typeof get; - getBatch: typeof getBatch; - list: typeof list; - put: typeof put; - putBatch: typeof putBatch; - delete: typeof delete_; - deleteBatch: typeof deleteBatch; - deleteAll: typeof deleteAll; -}; diff --git a/sdks/actor/bridge/types/90_rivet_ns.d.ts b/sdks/actor/bridge/types/90_rivet_ns.d.ts deleted file mode 100644 index 6dc7640701..0000000000 --- a/sdks/actor/bridge/types/90_rivet_ns.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// DO NOT MODIFY -// -// Generated from sdks/actor/bridge/ - -import type { Metadata } from "./types/metadata.d.ts"; -export type { Metadata } from "./types/metadata.d.ts"; -export declare function deepFreeze(object: T): Readonly; -export declare const ACTOR_CONTEXT: { - metadata: Metadata; - kv: { - get: ( - key: K, - options?: import("./40_rivet_kv.d.ts").GetOptions, - ) => Promise; - getBatch: , V>( - keys: K, - options?: import("./40_rivet_kv.d.ts").GetBatchOptions, - ) => Promise>; - list: ( - options?: import("./40_rivet_kv.d.ts").ListOptions, - ) => Promise>; - put: ( - key: K, - value: V | ArrayBuffer, - options?: import("./40_rivet_kv.d.ts").PutOptions, - ) => Promise; - putBatch: ( - obj: Map, - options?: import("./40_rivet_kv.d.ts").PutBatchOptions, - ) => Promise; - delete: (key: K) => Promise; - deleteBatch: >(keys: K) => Promise; - deleteAll: () => Promise; - }; -}; diff --git a/sdks/actor/bridge/types/types/metadata.d.ts b/sdks/actor/bridge/types/types/metadata.d.ts deleted file mode 100644 index 7358904e5b..0000000000 --- a/sdks/actor/bridge/types/types/metadata.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -// DO NOT MODIFY -// -// Generated from sdks/actor/bridge/ - -export interface Metadata { - actor: { - id: string; - tags: Record; - createdAt: Date; - }; - project: { - id: string; - slug: string; - }; - environment: { - id: string; - slug: string; - }; - cluster: { - id: string; - }; - region: { - id: string; - name: string; - }; - build: { - id: string; - }; -} - -export type InKey = { - jsInKey: Uint8Array[]; -}; -export type OutKey = { - inKey: Uint8Array[]; - outKey: Uint8Array[]; -}; -export type OutEntry = { - value: Uint8Array; - metadata: KeyMetadata; -}; -export type KeyMetadata = { - kvVersion: Uint8Array; - createTs: number; -}; -export type ListQuery = { - // Empty object - all?: Record; - rangeInclusive?: [Uint8Array[], InKey]; - rangeExclusive?: [Uint8Array[], InKey]; - prefix?: Uint8Array[]; -}; diff --git a/sdks/actor/core/src/bridge_types/40_rivet_kv.d.ts b/sdks/actor/core/src/bridge_types/40_rivet_kv.d.ts index 3144b8a240..5029d7233c 100644 --- a/sdks/actor/core/src/bridge_types/40_rivet_kv.d.ts +++ b/sdks/actor/core/src/bridge_types/40_rivet_kv.d.ts @@ -24,7 +24,7 @@ export interface GetBatchOptions { declare function getBatch, V>( keys: K, options?: GetBatchOptions, -): Promise>; +): Promise>; /** * Options for the `list` function. */ @@ -42,9 +42,9 @@ export interface ListOptions { * is used for filtering. * * @param {ListOptions} [options] - Options. - * @returns {Promise>} The retrieved values. + * @returns {Promise>} The retrieved values. */ -declare function list(options?: ListOptions): Promise>; +declare function list(options?: ListOptions): Promise>; /** * Options for the `put` function. */ @@ -101,6 +101,19 @@ declare function deleteBatch>(keys: K): Promise; * @returns {Promise} A promise that resolves when the operation is complete. */ declare function deleteAll(): Promise; +declare class HashMap { + #private; + constructor(internal: [K, V][]); + get(key: K): V | undefined; + /** + * Returns a map of keys to values. **WARNING** Using `.get` on the returned map does not work as expected + * with complex types (arrays, objects, etc). Use `.get` on this class instead. + */ + raw(): Map; + array(): [K, V][]; + entries(): ArrayIterator<[K, V]>; + [Symbol.iterator](): ArrayIterator<[K, V]>; +} export declare const KV_NAMESPACE: { get: typeof get; getBatch: typeof getBatch; diff --git a/sdks/actor/core/src/bridge_types/90_rivet_ns.d.ts b/sdks/actor/core/src/bridge_types/90_rivet_ns.d.ts index 6dc7640701..f1148fa7fd 100644 --- a/sdks/actor/core/src/bridge_types/90_rivet_ns.d.ts +++ b/sdks/actor/core/src/bridge_types/90_rivet_ns.d.ts @@ -15,10 +15,24 @@ export declare const ACTOR_CONTEXT: { getBatch: , V>( keys: K, options?: import("./40_rivet_kv.d.ts").GetBatchOptions, - ) => Promise>; + ) => Promise<{ + "__#1@#internal": [K[number], V][]; + get(key: K[number]): V | undefined; + raw(): Map; + array(): [K[number], V][]; + entries(): ArrayIterator<[K[number], V]>; + [Symbol.iterator](): ArrayIterator<[K[number], V]>; + }>; list: ( options?: import("./40_rivet_kv.d.ts").ListOptions, - ) => Promise>; + ) => Promise<{ + "__#1@#internal": [K, V][]; + get(key: K): V | undefined; + raw(): Map; + array(): [K, V][]; + entries(): ArrayIterator<[K, V]>; + [Symbol.iterator](): ArrayIterator<[K, V]>; + }>; put: ( key: K, value: V | ArrayBuffer, diff --git a/sdks/actor/core/src/bridge_types/lib/fast-equals/comparator.d.ts b/sdks/actor/core/src/bridge_types/lib/fast-equals/comparator.d.ts new file mode 100644 index 0000000000..157facb5f0 --- /dev/null +++ b/sdks/actor/core/src/bridge_types/lib/fast-equals/comparator.d.ts @@ -0,0 +1,30 @@ +// DO NOT MODIFY +// +// Generated from sdks/actor/bridge/ + +import type { ComparatorConfig, CreateState, CustomEqualCreatorOptions, EqualityComparator, InternalEqualityComparator } from "./internalTypes.js"; +interface CreateIsEqualOptions { + circular: boolean; + comparator: EqualityComparator; + createState: CreateState | undefined; + equals: InternalEqualityComparator; + strict: boolean; +} +/** + * Create a comparator method based on the type-specific equality comparators passed. + */ +export declare function createEqualityComparator({ areArraysEqual, areDatesEqual, areMapsEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, }: ComparatorConfig): EqualityComparator; +/** + * Create the configuration object used for building comparators. + */ +export declare function createEqualityComparatorConfig({ circular, createCustomConfig, strict, }: CustomEqualCreatorOptions): ComparatorConfig; +/** + * Default equality comparator pass-through, used as the standard `isEqual` creator for + * use inside the built comparator. + */ +export declare function createInternalEqualityComparator(compare: EqualityComparator): InternalEqualityComparator; +/** + * Create the `isEqual` function used by the consuming application. + */ +export declare function createIsEqual({ circular, comparator, createState, equals, strict, }: CreateIsEqualOptions): (a: A, b: B) => boolean; +export {}; diff --git a/sdks/actor/core/src/bridge_types/lib/fast-equals/equals.d.ts b/sdks/actor/core/src/bridge_types/lib/fast-equals/equals.d.ts new file mode 100644 index 0000000000..531b4a5fe8 --- /dev/null +++ b/sdks/actor/core/src/bridge_types/lib/fast-equals/equals.d.ts @@ -0,0 +1,41 @@ +// DO NOT MODIFY +// +// Generated from sdks/actor/bridge/ + +import type { Dictionary, PrimitiveWrapper, State, TypedArray } from "./internalTypes.js"; +/** + * Whether the arrays are equal in value. + */ +export declare function areArraysEqual(a: any[], b: any[], state: State): boolean; +/** + * Whether the dates passed are equal in value. + */ +export declare function areDatesEqual(a: Date, b: Date): boolean; +/** + * Whether the `Map`s are equal in value. + */ +export declare function areMapsEqual(a: Map, b: Map, state: State): boolean; +/** + * Whether the objects are equal in value. + */ +export declare function areObjectsEqual(a: Dictionary, b: Dictionary, state: State): boolean; +/** + * Whether the objects are equal in value with strict property checking. + */ +export declare function areObjectsEqualStrict(a: Dictionary, b: Dictionary, state: State): boolean; +/** + * Whether the primitive wrappers passed are equal in value. + */ +export declare function arePrimitiveWrappersEqual(a: PrimitiveWrapper, b: PrimitiveWrapper): boolean; +/** + * Whether the regexps passed are equal in value. + */ +export declare function areRegExpsEqual(a: RegExp, b: RegExp): boolean; +/** + * Whether the `Set`s are equal in value. + */ +export declare function areSetsEqual(a: Set, b: Set, state: State): boolean; +/** + * Whether the TypedArray instances are equal in value. + */ +export declare function areTypedArraysEqual(a: TypedArray, b: TypedArray): boolean; diff --git a/sdks/actor/core/src/bridge_types/lib/fast-equals/index.d.ts b/sdks/actor/core/src/bridge_types/lib/fast-equals/index.d.ts new file mode 100644 index 0000000000..72607ad737 --- /dev/null +++ b/sdks/actor/core/src/bridge_types/lib/fast-equals/index.d.ts @@ -0,0 +1,51 @@ +// DO NOT MODIFY +// +// Generated from sdks/actor/bridge/ + +import type { CustomEqualCreatorOptions } from "./internalTypes.js"; +import { sameValueZeroEqual } from "./utils.js"; +export { sameValueZeroEqual }; +export * from "./internalTypes.js"; +/** + * Whether the items passed are deeply-equal in value. + */ +export declare const deepEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are deeply-equal in value based on strict comparison. + */ +export declare const strictDeepEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are deeply-equal in value, including circular references. + */ +export declare const circularDeepEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are deeply-equal in value, including circular references, + * based on strict comparison. + */ +export declare const strictCircularDeepEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are shallowly-equal in value. + */ +export declare const shallowEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are shallowly-equal in value based on strict comparison + */ +export declare const strictShallowEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are shallowly-equal in value, including circular references. + */ +export declare const circularShallowEqual: (a: A, b: B) => boolean; +/** + * Whether the items passed are shallowly-equal in value, including circular references, + * based on strict comparison. + */ +export declare const strictCircularShallowEqual: (a: A, b: B) => boolean; +/** + * Create a custom equality comparison method. + * + * This can be done to create very targeted comparisons in extreme hot-path scenarios + * where the standard methods are not performant enough, but can also be used to provide + * support for legacy environments that do not support expected features like + * `RegExp.prototype.flags` out of the box. + */ +export declare function createCustomEqual(options?: CustomEqualCreatorOptions): (a: A, b: B) => boolean; diff --git a/sdks/actor/core/src/bridge_types/lib/fast-equals/internalTypes.d.ts b/sdks/actor/core/src/bridge_types/lib/fast-equals/internalTypes.d.ts new file mode 100644 index 0000000000..4060b99242 --- /dev/null +++ b/sdks/actor/core/src/bridge_types/lib/fast-equals/internalTypes.d.ts @@ -0,0 +1,138 @@ +// DO NOT MODIFY +// +// Generated from sdks/actor/bridge/ + +/** + * Cache used to store references to objects, used for circular + * reference checks. + */ +export interface Cache { + delete(key: Key): boolean; + get(key: Key): Value | undefined; + set(key: Key, value: any): any; +} +export interface State { + /** + * Cache used to identify circular references + */ + readonly cache: Cache | undefined; + /** + * Method used to determine equality of nested value. + */ + readonly equals: InternalEqualityComparator; + /** + * Additional value that can be used for comparisons. + */ + meta: Meta; + /** + * Whether the equality comparison is strict, meaning it matches + * all properties (including symbols and non-enumerable properties) + * with equal shape of descriptors. + */ + readonly strict: boolean; +} +export interface CircularState extends State { + readonly cache: Cache; +} +export interface DefaultState extends State { + readonly cache: undefined; +} +export interface Dictionary { + [key: string | symbol]: Value; + $$typeof?: any; +} +export interface ComparatorConfig { + /** + * Whether the arrays passed are equal in value. In strict mode, this includes + * additional properties added to the array. + */ + areArraysEqual: TypeEqualityComparator; + /** + * Whether the dates passed are equal in value. + */ + areDatesEqual: TypeEqualityComparator; + /** + * Whether the maps passed are equal in value. In strict mode, this includes + * additional properties added to the map. + */ + areMapsEqual: TypeEqualityComparator; + /** + * Whether the objects passed are equal in value. In strict mode, this includes + * non-enumerable properties added to the map, as well as symbol properties. + */ + areObjectsEqual: TypeEqualityComparator; + /** + * Whether the primitive wrappers passed are equal in value. + */ + arePrimitiveWrappersEqual: TypeEqualityComparator; + /** + * Whether the regexps passed are equal in value. + */ + areRegExpsEqual: TypeEqualityComparator; + /** + * Whether the sets passed are equal in value. In strict mode, this includes + * additional properties added to the set. + */ + areSetsEqual: TypeEqualityComparator; + /** + * Whether the typed arrays passed are equal in value. In strict mode, this includes + * additional properties added to the typed array. + */ + areTypedArraysEqual: TypeEqualityComparator; +} +export type CreateCustomComparatorConfig = (config: ComparatorConfig) => Partial>; +export type CreateState = () => { + cache?: Cache | undefined; + meta?: Meta; +}; +export type EqualityComparator = (a: A, b: B, state: State) => boolean; +export type AnyEqualityComparator = (a: any, b: any, state: State) => boolean; +export type EqualityComparatorCreator = (fn: EqualityComparator) => InternalEqualityComparator; +export type InternalEqualityComparator = (a: any, b: any, indexOrKeyA: any, indexOrKeyB: any, parentA: any, parentB: any, state: State) => boolean; +export type PrimitiveWrapper = Boolean | Number | String; +/** + * Type which encompasses possible instances of TypedArray + * classes. + * + * **NOTE**: This does not include `BigInt64Array` and + * `BitUint64Array` because those are part of ES2020 and + * not supported by certain TS configurations. If using + * either in `areTypedArraysEqual`, you can cast the + * instance as `TypedArray` and it will work as expected, + * because runtime checks will still work for those classes. + */ +export type TypedArray = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint16Array | Uint32Array | Uint8Array | Uint8ClampedArray; +export type TypeEqualityComparator = (a: Type, b: Type, state: State) => boolean; +export interface CustomEqualCreatorOptions { + /** + * Whether circular references should be supported. It causes the + * comparison to be slower, but for objects that have circular references + * it is required to avoid stack overflows. + */ + circular?: boolean; + /** + * Create a custom configuration of type-specific equality comparators. + * This receives the default configuration, which allows either replacement + * or supersetting of the default methods. + */ + createCustomConfig?: CreateCustomComparatorConfig; + /** + * Create a custom internal comparator, which is used as an override to the + * default entry point for nested value equality comparisons. This is often + * used for doing custom logic for specific types (such as handling a specific + * class instance differently than other objects) or to incorporate `meta` in + * the comparison. See the recipes for examples. + */ + createInternalComparator?: (compare: EqualityComparator) => InternalEqualityComparator; + /** + * Create a custom `state` object passed between the methods. This allows for + * custom `cache` and/or `meta` values to be used. + */ + createState?: CreateState; + /** + * Whether the equality comparison is strict, meaning it matches + * all properties (including symbols and non-enumerable properties) + * with equal shape of descriptors. + */ + strict?: boolean; +} diff --git a/sdks/actor/core/src/bridge_types/lib/fast-equals/utils.d.ts b/sdks/actor/core/src/bridge_types/lib/fast-equals/utils.d.ts new file mode 100644 index 0000000000..e7619500ca --- /dev/null +++ b/sdks/actor/core/src/bridge_types/lib/fast-equals/utils.d.ts @@ -0,0 +1,28 @@ +// DO NOT MODIFY +// +// Generated from sdks/actor/bridge/ + +import { AnyEqualityComparator, Dictionary, State, TypeEqualityComparator } from "./internalTypes.js"; +/** + * Combine two comparators into a single comparators. + */ +export declare function combineComparators(comparatorA: AnyEqualityComparator, comparatorB: AnyEqualityComparator): (a: A, b: B, state: State) => boolean; +/** + * Wrap the provided `areItemsEqual` method to manage the circular state, allowing + * for circular references to be safely included in the comparison without creating + * stack overflows. + */ +export declare function createIsCircular>(areItemsEqual: AreItemsEqual): AreItemsEqual; +/** + * Get the properties to strictly examine, which include both own properties that are + * not enumerable and symbol properties. + */ +export declare function getStrictProperties(object: Dictionary): Array; +/** + * Whether the object contains the property passed as an own property. + */ +export declare const hasOwn: (o: object, v: PropertyKey) => boolean; +/** + * Whether the values passed are strictly equal or both NaN. + */ +export declare function sameValueZeroEqual(a: any, b: any): boolean;