New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@tldraw/tlstore

Package Overview
Dependencies
Maintainers
4
Versions
328
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tldraw/tlstore - npm Package Compare versions

Comparing version 2.0.0-canary.dd436e7df to 2.0.0-canary.ddca01918fab

src/lib/test/recordType.test.ts

78

dist-cjs/index.d.ts
import { Atom } from 'signia';
import { Computed } from 'signia';
import { Signal } from 'signia';

@@ -55,6 +56,6 @@ /**

/** @public */
export declare function compareRecordVersions(a: RecordVersion, b: RecordVersion): 0 | 1 | -1;
export declare function compareRecordVersions(a: RecordVersion, b: RecordVersion): -1 | 0 | 1;
/** @public */
export declare const compareSchemas: (a: SerializedSchema, b: SerializedSchema) => number;
export declare const compareSchemas: (a: SerializedSchema, b: SerializedSchema) => -1 | 0 | 1;

@@ -85,2 +86,3 @@ /**

validator: StoreValidator<R>;
scope: Scope;
}): RecordType<R, keyof Omit<R, 'id' | 'typeName'>>;

@@ -131,3 +133,3 @@

changes: RecordsDiff<R>;
source: 'user' | 'remote';
source: 'remote' | 'user';
};

@@ -176,7 +178,7 @@

export declare type MigrationResult<T> = {
type: 'error';
reason: MigrationFailureReason;
} | {
type: 'success';
value: T;
} | {
type: 'error';
reason: MigrationFailureReason;
};

@@ -196,3 +198,3 @@

declare type Range_2<From extends number, To extends number> = To extends From ? From : To | Range_2<From, Decrement<To>>;
declare type Range_2<From extends number, To extends number> = To extends From ? From : Range_2<From, Decrement<To>> | To;

@@ -228,5 +230,6 @@ declare type RecFromId<K extends ID> = K extends ID<infer R> ? R : never;

readonly migrations: Migrations;
readonly validator: StoreValidator<R> | {
readonly validator: {
validate: (r: unknown) => R;
};
} | StoreValidator<R>;
readonly scope: Scope;
constructor(

@@ -242,5 +245,6 @@ /**

readonly migrations: Migrations;
readonly validator?: StoreValidator<R> | {
readonly validator?: {
validate: (r: unknown) => R;
};
} | StoreValidator<R>;
readonly scope?: Scope;
});

@@ -289,2 +293,9 @@ /**

/**
* Takes an id like `user:123` and returns the part after the colon `123`
*
* @param id - The id
* @returns
*/
parseId(id: string): ID<R>;
/**
* Check whether a record is an instance of this record type.

@@ -329,3 +340,3 @@ *

*/
withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'typeName' | 'id'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>;
withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'id' | 'typeName'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>;
/**

@@ -351,2 +362,13 @@ * Check that the passed in record passes the validations for this type. Returns its input

/**
* Defines the scope of the record
*
* instance: The record belongs to a single instance of the store. It should not be synced, and any persistence logic should 'de-instance-ize' the record before persisting it, and apply the reverse when rehydrating.
* document: The record is persisted and synced. It is available to all store instances.
* presence: The record belongs to a single instance of the store. It may be synced to other instances, but other instances should not make changes to it. It should not be persisted.
*
* @public
* */
declare type Scope = 'document' | 'instance' | 'presence';
/** @public */

@@ -364,6 +386,6 @@ export declare interface SerializedSchema {

version: number;
subTypeVersions: Record<string, number>;
subTypeKey: string;
} | {
version: number;
subTypeVersions: Record<string, number>;
subTypeKey: string;
}>;

@@ -424,3 +446,3 @@ }

private updateHistory;
validate(phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests'): void;
validate(phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord'): void;
/**

@@ -489,2 +511,7 @@ * A callback fired after a record is created. Use this to perform related updates to other

/**
* The same as `serialize`, but only serializes records with a scope of `document`.
* @returns The record store snapshot as a JSON payload.
*/
serializeDocumentState: () => StoreSnapshot<R>;
/**
* Opposite of `serialize`. Replace the store's current records with records as defined by a

@@ -559,2 +586,3 @@ * simple JSON structure into the stores.

createSelectedComputedCache: <T, J, V extends R = R>(name: string, selector: (record: V) => T | undefined, derive: (input: T) => J | undefined) => ComputedCache<J, V>;
private _integrityChecker?;
/* Excluded from this release type: ensureStoreIsUsable */

@@ -569,3 +597,3 @@ private _isPossiblyCorrupted;

error: Error;
phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests';
phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord';
recordBefore?: unknown;

@@ -676,6 +704,7 @@ recordAfter: unknown;

get currentStoreVersion(): number;
validateRecord(store: Store<R>, record: R, phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests', recordBefore: R | null): R;
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'up' | 'down'): MigrationResult<R>;
validateRecord(store: Store<R>, record: R, phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord', recordBefore: null | R): R;
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult<R>;
migrateStoreSnapshot(storeSnapshot: StoreSnapshot<R>, persistedSchema: SerializedSchema): MigrationResult<StoreSnapshot<R>>;
/* Excluded from this release type: ensureStoreIsUsable */
/* Excluded from this release type: createIntegrityChecker */
/* Excluded from this release type: derivePresenceState */
serialize(): SerializedSchema;

@@ -694,6 +723,7 @@ serializeEarliestVersion(): SerializedSchema;

record: R;
phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests';
recordBefore: R | null;
phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord';
recordBefore: null | R;
}) => R;
/* Excluded from this release type: ensureStoreIsUsable */
/* Excluded from this release type: createIntegrityChecker */
/* Excluded from this release type: derivePresenceState */
};

@@ -723,7 +753,7 @@

} | {
gt: number;
} | {
neq: T;
} | {
gt: number;
};
export { }

@@ -6,2 +6,6 @@ "use strict";

var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {

@@ -15,7 +19,30 @@ if (from && typeof from === "object" || typeof from === "function") {

};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var src_exports = {};
__export(src_exports, {
IncrementalSetConstructor: () => import_IncrementalSetConstructor.IncrementalSetConstructor,
MigrationFailureReason: () => import_migrate.MigrationFailureReason,
RecordType: () => import_RecordType.RecordType,
Store: () => import_Store.Store,
StoreSchema: () => import_StoreSchema.StoreSchema,
assertIdType: () => import_RecordType.assertIdType,
compareRecordVersions: () => import_migrate.compareRecordVersions,
compareSchemas: () => import_compareSchemas.compareSchemas,
createRecordType: () => import_RecordType.createRecordType,
defineMigrations: () => import_migrate.defineMigrations,
devFreeze: () => import_devFreeze.devFreeze,
getRecordVersion: () => import_migrate.getRecordVersion,
migrate: () => import_migrate.migrate,
migrateRecord: () => import_migrate.migrateRecord,
reverseRecordsDiff: () => import_Store.reverseRecordsDiff,
squashRecordDiffs: () => import_Store.squashRecordDiffs
});
module.exports = __toCommonJS(src_exports);
__reExport(src_exports, require("./lib"), module.exports);
var import_IncrementalSetConstructor = require("./lib/IncrementalSetConstructor");
var import_RecordType = require("./lib/RecordType");
var import_Store = require("./lib/Store");
var import_StoreSchema = require("./lib/StoreSchema");
var import_compareSchemas = require("./lib/compareSchemas");
var import_devFreeze = require("./lib/devFreeze");
var import_migrate = require("./lib/migrate");
//# sourceMappingURL=index.js.map

@@ -34,2 +34,3 @@ "use strict";

this.validator = config.validator ?? { validate: (r) => r };
this.scope = config.scope ?? "document";
}

@@ -39,2 +40,3 @@ createDefaultProperties;

validator;
scope;
/**

@@ -97,2 +99,14 @@ * Create a new record of this type.

/**
* Takes an id like `user:123` and returns the part after the colon `123`
*
* @param id - The id
* @returns
*/
parseId(id) {
if (!this.isId(id)) {
throw new Error(`ID "${id}" is not a valid ID for type "${this.typeName}"`);
}
return id.slice(this.typeName.length + 1);
}
/**
* Check whether a record is an instance of this record type.

@@ -151,3 +165,4 @@ *

migrations: this.migrations,
validator: this.validator
validator: this.validator,
scope: this.scope
});

@@ -167,3 +182,4 @@ }

migrations: config.migrations ?? { currentVersion: 0, firstVersion: 0, migrators: {} },
validator: config.validator
validator: config.validator,
scope: config.scope
});

@@ -170,0 +186,0 @@ }

@@ -26,2 +26,3 @@ "use strict";

module.exports = __toCommonJS(Store_exports);
var import_utils = require("@tldraw/utils");
var import_signia = require("signia");

@@ -96,3 +97,3 @@ var import_Cache = require("./Cache");

},
{ scheduleEffect: (cb) => requestAnimationFrame(cb) }
{ scheduleEffect: (cb) => (0, import_utils.throttledRaf)(cb) }
);

@@ -299,2 +300,12 @@ }

/**
* The same as `serialize`, but only serializes records with a scope of `document`.
* @returns The record store snapshot as a JSON payload.
*/
serializeDocumentState = () => {
return this.serialize((r) => {
const type = this.schema.types[r.typeName];
return type.scope === "document";
});
};
/**
* Opposite of `serialize`. Replace the store's current records with records as defined by a

@@ -469,5 +480,7 @@ * simple JSON structure into the stores.

};
_integrityChecker;
/** @internal */
ensureStoreIsUsable() {
this.schema.ensureStoreIsUsable(this);
this._integrityChecker ??= this.schema.createIntegrityChecker(this);
this._integrityChecker?.();
}

@@ -578,13 +591,3 @@ _isPossiblyCorrupted = false;

}
/**
* Ensure that the store is usable. A class that extends this store should override this method.
*
* @param config - The configuration object. This can be any object that allows the store to
* validate that it is usable; the extending class should specify the type.
* @public
*/
ensureStoreIsUsable(_config = {}) {
return;
}
}
//# sourceMappingURL=Store.js.map

@@ -150,5 +150,9 @@ "use strict";

/** @internal */
ensureStoreIsUsable(store) {
this.options.ensureStoreIsUsable?.(store);
createIntegrityChecker(store) {
return this.options.createIntegrityChecker?.(store) ?? void 0;
}
/** @internal */
derivePresenceState(store) {
return this.options.derivePresenceState?.(store);
}
serialize() {

@@ -155,0 +159,0 @@ return {

{
"name": "@tldraw/tlstore",
"description": "A tiny little drawing app (store).",
"version": "2.0.0-canary.dd436e7df",
"version": "2.0.0-canary.ddca01918fab",
"packageManager": "yarn@3.5.0",
"author": {

@@ -34,15 +35,15 @@ "name": "tldraw GB Ltd.",

"scripts": {
"test": "yarn run -T jest",
"test:coverage": "yarn run -T jest --coverage",
"build": "echo 'build should be run by turbo'",
"build:package": "yarn run -T tsx ../../scripts/build-package.ts",
"build:api": "yarn run -T tsx ../../scripts/build-api.ts",
"test": "lazy inherit",
"test-coverage": "lazy inherit",
"build": "yarn run -T tsx ../../scripts/build-package.ts",
"build-api": "yarn run -T tsx ../../scripts/build-api.ts",
"prepack": "yarn run -T tsx ../../scripts/prepack.ts",
"postpack": "../../scripts/postpack.sh",
"pack-tarball": "yarn pack",
"lint": "yarn run -T tsx ../../scripts/lint.ts"
},
"dependencies": {
"@tldraw/utils": "2.0.0-canary.dd436e7df",
"@tldraw/utils": "2.0.0-canary.ddca01918fab",
"lodash.isequal": "^4.5.0",
"nanoid": "^3.0.0"
"nanoid": "4.0.2"
},

@@ -55,2 +56,3 @@ "peerDependencies": {

"@types/lodash.isequal": "^4.5.6",
"lazyrepo": "0.0.0-alpha.26",
"raf": "^3.4.1"

@@ -57,0 +59,0 @@ },

@@ -351,1 +351,5 @@ # @tldraw/tlstore

A diff describing the changes to a collection.
## License
The source code in this repository (as well as our 2.0+ distributions and releases) are currently licensed under Apache-2.0. These licenses are subject to change in our upcoming 2.0 release. If you are planning to use tldraw in a commercial product, please reach out at [hello@tldraw.com](mailto://hello@tldraw.com).

@@ -1,1 +0,32 @@

export * from './lib'
export type { BaseRecord, ID } from './lib/BaseRecord'
export { IncrementalSetConstructor } from './lib/IncrementalSetConstructor'
export { RecordType, assertIdType, createRecordType } from './lib/RecordType'
export { Store, reverseRecordsDiff, squashRecordDiffs } from './lib/Store'
export type {
CollectionDiff,
ComputedCache,
HistoryEntry,
RecordsDiff,
StoreError,
StoreListener,
StoreSnapshot,
StoreValidator,
StoreValidators,
} from './lib/Store'
export { StoreSchema } from './lib/StoreSchema'
export type { SerializedSchema, StoreSchemaOptions } from './lib/StoreSchema'
export { compareSchemas } from './lib/compareSchemas'
export { devFreeze } from './lib/devFreeze'
export {
MigrationFailureReason,
compareRecordVersions,
defineMigrations,
getRecordVersion,
migrate,
migrateRecord,
type Migration,
type MigrationResult,
type Migrations,
type RecordVersion,
} from './lib/migrate'
export type { AllRecords } from './lib/type-utils'
import { SerializedSchema } from './StoreSchema'
/** @public */
export const compareSchemas = (a: SerializedSchema, b: SerializedSchema): number => {
export const compareSchemas = (a: SerializedSchema, b: SerializedSchema): 0 | 1 | -1 => {
if (a.schemaVersion > b.schemaVersion) {

@@ -6,0 +6,0 @@ return 1

@@ -10,2 +10,13 @@ import { structuredClone } from '@tldraw/utils'

/**
* Defines the scope of the record
*
* instance: The record belongs to a single instance of the store. It should not be synced, and any persistence logic should 'de-instance-ize' the record before persisting it, and apply the reverse when rehydrating.
* document: The record is persisted and synced. It is available to all store instances.
* presence: The record belongs to a single instance of the store. It may be synced to other instances, but other instances should not make changes to it. It should not be persisted.
*
* @public
* */
export type Scope = 'instance' | 'document' | 'presence'
/**
* A record type is a type that can be stored in a record store. It is created with

@@ -24,2 +35,4 @@ * `createRecordType`.

readonly scope: Scope
constructor(

@@ -37,2 +50,3 @@ /**

readonly validator?: StoreValidator<R> | { validate: (r: unknown) => R }
readonly scope?: Scope
}

@@ -43,2 +57,3 @@ ) {

this.validator = config.validator ?? { validate: (r: unknown) => r as R }
this.scope = config.scope ?? 'document'
}

@@ -110,2 +125,16 @@

/**
* Takes an id like `user:123` and returns the part after the colon `123`
*
* @param id - The id
* @returns
*/
parseId(id: string): ID<R> {
if (!this.isId(id)) {
throw new Error(`ID "${id}" is not a valid ID for type "${this.typeName}"`)
}
return id.slice(this.typeName.length + 1) as ID<R>
}
/**
* Check whether a record is an instance of this record type.

@@ -168,2 +197,3 @@ *

validator: this.validator,
scope: this.scope,
})

@@ -199,2 +229,3 @@ }

validator: StoreValidator<R>
scope: Scope
}

@@ -206,2 +237,3 @@ ): RecordType<R, keyof Omit<R, 'id' | 'typeName'>> {

validator: config.validator,
scope: config.scope,
})

@@ -208,0 +240,0 @@ }

@@ -0,1 +1,2 @@

import { throttledRaf } from '@tldraw/utils'
import { atom, Atom, computed, Computed, Reactor, reactor, transact } from 'signia'

@@ -5,2 +6,3 @@ import { BaseRecord, ID } from './BaseRecord'

import { devFreeze } from './devFreeze'
import { RecordType } from './RecordType'
import { StoreQueries } from './StoreQueries'

@@ -176,3 +178,3 @@ import { StoreSchema } from './StoreSchema'

},
{ scheduleEffect: (cb) => requestAnimationFrame(cb) }
{ scheduleEffect: (cb) => throttledRaf(cb) }
)

@@ -434,2 +436,13 @@ }

/**
* The same as `serialize`, but only serializes records with a scope of `document`.
* @returns The record store snapshot as a JSON payload.
*/
serializeDocumentState = (): StoreSnapshot<R> => {
return this.serialize((r) => {
const type = this.schema.types[r.typeName as R['typeName']] as RecordType<any, any>
return type.scope === 'document'
})
}
/**
* Opposite of `serialize`. Replace the store's current records with records as defined by a

@@ -627,5 +640,8 @@ * simple JSON structure into the stores.

private _integrityChecker?: () => void | undefined
/** @internal */
ensureStoreIsUsable() {
this.schema.ensureStoreIsUsable(this)
this._integrityChecker ??= this.schema.createIntegrityChecker(this)
this._integrityChecker?.()
}

@@ -773,13 +789,2 @@

}
/**
* Ensure that the store is usable. A class that extends this store should override this method.
*
* @param config - The configuration object. This can be any object that allows the store to
* validate that it is usable; the extending class should specify the type.
* @public
*/
ensureStoreIsUsable(_config = {} as any): void {
return
}
}
import { getOwnProperty, objectMapValues } from '@tldraw/utils'
import { Signal } from 'signia'
import { BaseRecord } from './BaseRecord'

@@ -50,3 +51,5 @@ import { RecordType } from './RecordType'

/** @internal */
ensureStoreIsUsable?: (store: Store<R, P>) => void
createIntegrityChecker?: (store: Store<R, P>) => void
/** @internal */
derivePresenceState?: (store: Store<R, P>) => Signal<R | null>
}

@@ -241,6 +244,11 @@

/** @internal */
ensureStoreIsUsable(store: Store<R, P>): void {
this.options.ensureStoreIsUsable?.(store)
createIntegrityChecker(store: Store<R, P>): (() => void) | undefined {
return this.options.createIntegrityChecker?.(store) ?? undefined
}
/** @internal */
derivePresenceState(store: Store<R, P>): Signal<R | null> | undefined {
return this.options.derivePresenceState?.(store)
}
serialize(): SerializedSchema {

@@ -247,0 +255,0 @@ return {

@@ -13,3 +13,6 @@ import { Computed, react, RESET_VALUE, transact } from 'signia'

const Book = createRecordType<Book>('book', { validator: { validate: (book) => book as Book } })
const Book = createRecordType<Book>('book', {
validator: { validate: (book) => book as Book },
scope: 'document',
})

@@ -23,2 +26,3 @@ interface Author extends BaseRecord<'author'> {

validator: { validate: (author) => author as Author },
scope: 'document',
}).withDefaultProperties(() => ({

@@ -470,22 +474,29 @@ isPseudonym: false,

it('flushes history before attaching listeners', async () => {
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createCustomId('tolkein') })])
const firstListener = jest.fn()
store.listen(firstListener)
expect(firstListener).toHaveBeenCalledTimes(0)
try {
// @ts-expect-error
globalThis.__FORCE_RAF_IN_TESTS__ = true
store.put([Author.create({ name: 'J.R.R Tolkein', id: Author.createCustomId('tolkein') })])
const firstListener = jest.fn()
store.listen(firstListener)
expect(firstListener).toHaveBeenCalledTimes(0)
store.put([Author.create({ name: 'Chips McCoy', id: Author.createCustomId('chips') })])
store.put([Author.create({ name: 'Chips McCoy', id: Author.createCustomId('chips') })])
expect(firstListener).toHaveBeenCalledTimes(0)
expect(firstListener).toHaveBeenCalledTimes(0)
const secondListener = jest.fn()
const secondListener = jest.fn()
store.listen(secondListener)
store.listen(secondListener)
expect(firstListener).toHaveBeenCalledTimes(1)
expect(secondListener).toHaveBeenCalledTimes(0)
expect(firstListener).toHaveBeenCalledTimes(1)
expect(secondListener).toHaveBeenCalledTimes(0)
await new Promise((resolve) => requestAnimationFrame(resolve))
await new Promise((resolve) => requestAnimationFrame(resolve))
expect(firstListener).toHaveBeenCalledTimes(1)
expect(secondListener).toHaveBeenCalledTimes(0)
expect(firstListener).toHaveBeenCalledTimes(1)
expect(secondListener).toHaveBeenCalledTimes(0)
} finally {
// @ts-expect-error
globalThis.__FORCE_RAF_IN_TESTS__ = false
}
})

@@ -492,0 +503,0 @@

@@ -24,2 +24,3 @@ import { atom, EffectScheduler, RESET_VALUE } from 'signia'

},
scope: 'document',
}).withDefaultProperties(() => ({ age: 23 }))

@@ -42,2 +43,3 @@

},
scope: 'document',
})

@@ -44,0 +46,0 @@

@@ -22,2 +22,3 @@ import { atom, RESET_VALUE } from 'signia'

},
scope: 'document',
}).withDefaultProperties(() => ({ age: 23 }))

@@ -40,2 +41,3 @@

},
scope: 'document',
})

@@ -42,0 +44,0 @@ const authors = {

@@ -32,2 +32,3 @@ import { assert } from '@tldraw/utils'

},
scope: 'document',
})

@@ -94,2 +95,3 @@

},
scope: 'document',
})

@@ -112,2 +114,3 @@

},
scope: 'document',
})

@@ -114,0 +117,0 @@

@@ -65,2 +65,3 @@ import { assert } from '@tldraw/utils'

},
scope: 'document',
}).withDefaultProperties(() => ({

@@ -196,2 +197,3 @@ /* STEP 6: Add any new default values for properties here */

},
scope: 'document',
}).withDefaultProperties(() => ({

@@ -198,0 +200,0 @@ x: 0,

@@ -24,2 +24,3 @@ import { BaseRecord, ID } from '../BaseRecord'

},
scope: 'document',
})

@@ -43,2 +44,3 @@

},
scope: 'document',
}).withDefaultProperties(() => ({

@@ -45,0 +47,0 @@ isPseudonym: false,

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc