feat tracker update zustand plugin for better ts support (#2020)

This commit is contained in:
Delirium 2024-04-02 15:56:29 +02:00 committed by GitHub
parent c70c164d1d
commit c69d1fe00e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 376 additions and 193 deletions

View file

@ -10,28 +10,32 @@ npm i @openreplay/tracker-zustand
Initialize the `@openreplay/tracker` package as usual and load the plugin into it. Initialize the `@openreplay/tracker` package as usual and load the plugin into it.
Then put the generated plugin into your `plugins` field of your store. Then put the generated plugin into your `plugins` field of your store.
```js ```ts
import create from "zustand"; import create from "zustand";
import Tracker from '@openreplay/tracker'; import Tracker from '@openreplay/tracker';
import trackerZustand from '@openreplay/tracker-zustand'; import trackerZustand, { StateLogger } from '@openreplay/tracker-zustand';
const tracker = new Tracker({ const tracker = new Tracker({
projectKey: YOUR_PROJECT_KEY, projectKey: YOUR_PROJECT_KEY,
}); });
const zustandPlugin = tracker.use(trackerZustand()) // as per https://docs.pmnd.rs/zustand/guides/typescript#middleware-that-doesn't-change-the-store-type
// store name, optional // cast type to new one
// randomly generated if undefined // but this seems to not be required and everything is working as is
const bearStoreLogger = zustandPlugin('bear_store') const zustandPlugin = tracker.use(trackerZustand()) as unknown as StateLogger
const useBearStore = create( const useBearStore = create(
bearStoreLogger((set: any) => ({ zustandPlugin((set: any) => ({
bears: 0, bears: 0,
increasePopulation: () => set((state: any) => ({ bears: state.bears + 1 })), increasePopulation: () => set((state: any) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }), removeAllBears: () => set({ bears: 0 }),
})) }),
// store name is optional
// and is randomly generated if undefined
'bear_store'
)
) )
``` ```

BIN
tracker/tracker-zustand/bun.lockb Executable file

Binary file not shown.

View file

@ -1,7 +1,7 @@
{ {
"name": "@openreplay/tracker-zustand", "name": "@openreplay/tracker-zustand",
"description": "Tracker plugin for Zustand state recording", "description": "Tracker plugin for Zustand state recording",
"version": "1.0.3", "version": "1.1.0",
"keywords": [ "keywords": [
"zustand", "zustand",
"state", "state",
@ -22,12 +22,12 @@
"build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs'", "build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs'",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"dependencies": {}, "dependencies": { "zustand": "^4.5.2" },
"peerDependencies": { "peerDependencies": {
"@openreplay/tracker": ">=4.0.1" "@openreplay/tracker": ">=12.0.0"
}, },
"devDependencies": { "devDependencies": {
"@openreplay/tracker": "^4.0.1", "@openreplay/tracker": "^12.0.0",
"prettier": "^1.18.2", "prettier": "^1.18.2",
"replace-in-files-cli": "^1.0.0", "replace-in-files-cli": "^1.0.0",
"typescript": "^4.6.0-dev.20211126" "typescript": "^4.6.0-dev.20211126"

View file

@ -1,68 +1,84 @@
import { App, Messages } from '@openreplay/tracker' import { App, Messages } from "@openreplay/tracker";
import { Encoder, sha1 } from './syncod/index.js' import { Encoder, murmur } from "./syncod-v2/src/index.js";
import { StateCreator, StoreMutatorIdentifier } from "zustand";
export type StateLogger = <
T extends unknown,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
f: StateCreator<T, Mps, Mcs>,
name?: string
) => StateCreator<T, Mps, Mcs>;
type LoggerImpl = <T extends unknown>(
f: StateCreator<T, [], []>,
name?: string
) => StateCreator<T, [], []>;
export interface Options { export interface Options {
filter: (mutation: any, state: any) => boolean filter: (mutation: any, state: any) => boolean;
transformer: (state: any) => any transformer: (state: any) => any;
mutationTransformer: (mutation: any) => any mutationTransformer: (mutation: any) => any;
} }
function processMutationAndState( function processMutationAndState(
app: App, app: App,
options: Options, options: Options,
encoder: Encoder, encoder: Encoder,
mutation: string[], mutation: string[],
state: Record<string, any>, state: Record<string, any>
) { ) {
if (options.filter(mutation, state)) { if (options.filter(mutation, state)) {
try { try {
const _mutation = encoder.encode(options.mutationTransformer(mutation)) const _mutation = encoder.encode(options.mutationTransformer(mutation));
const _state = encoder.encode(options.transformer(state)) const _state = encoder.encode(options.transformer(state));
const _table = encoder.commit() const _table = encoder.commit();
for (let key in _table) app.send(Messages.OTable(key, _table[key])) for (let key in _table) app.send(Messages.OTable(key, _table[key]));
app.send(Messages.Zustand(_mutation, _state)) app.send(Messages.Zustand(_mutation, _state));
} catch (e) { } catch (e) {
encoder.clear() encoder.clear();
app.debug.error(e) app.debug.error(e);
} }
} }
} }
export default function(opts: Partial<Options> = {}) { const createZustandTracker = (opts: Partial<Options> = {}) => {
const options: Options = Object.assign( const options: Options = Object.assign(
{ {
filter: () => true, filter: () => true,
transformer: (state) => state, transformer: state => state,
mutationTransformer: (mutation) => mutation, mutationTransformer: mutation => mutation
}, },
opts, opts
) );
return (app: App | null) => { return (app: App | null): LoggerImpl => {
if (app === null) { if (app === null) {
return Function.prototype return f => (set, get, api) => f(set, get, api);
} }
const encoder = new Encoder(sha1, 50) const encoder = new Encoder(murmur, 50);
const state = {} const state = {};
return (
storeName: string = Math.random()
.toString(36)
.substring(2, 9),
) => (config: Function) => (
set: (...args: any) => void,
get: () => Record<string, any>,
api: any,
) =>
config(
(...args) => {
set(...args)
const newState = get()
state[storeName] = newState
const triggeredActions = args.map((action) => action.toString?.())
processMutationAndState(app, options, encoder, triggeredActions, state) return (
}, f,
get, name = Math.random()
api, .toString(36)
) .substring(2, 9)
} ) => (set, get, api) => {
} const loggedSet: typeof set = (...args) => {
set(...args);
state[name] = get();
processMutationAndState(
app,
options,
encoder,
args.map(a => (a ? a.toString?.() ?? "" : "")),
state
);
};
return f(loggedSet, get, api);
};
};
};
export default createZustandTracker;

View file

@ -1,4 +1,4 @@
const chars = {}; const chars: Record<string, string> = {};
[ [
"DEL", "DEL",

View file

@ -0,0 +1,79 @@
import _ from "./chars.js";
export default class Decoder {
_dict: Map<any, any>;
constructor() {
this._dict = new Map();
}
set(ref, enc) {
this._dict.set(ref, enc);
}
assign(dict) {
for (let ref in dict) {
this._dict.set(ref, dict[ref]);
}
}
clear() {
this._dict.clear();
}
_unref_str(str) {
let s = this._dict.get(str);
if (s !== undefined) {
return s;
}
return str;
}
decode(enc) {
const p = enc[0],
b = enc.slice(1);
switch (p) {
case _.UNDEF:
return undefined;
case _.TRUE:
return true;
case _.FALSE:
return false;
case _.FUNCTION:
return Function.prototype;
case _.NUMBER:
return parseFloat(b);
case _.BIGINT:
return BigInt(b);
case _.STRING:
return this._unref_str(b);
case _.SYMBOL:
return Symbol(this._unref_str(b));
case _.NULL:
return null;
}
const unref = this._dict.get(b);
if (unref === undefined) {
throw "index missing code";
}
if (typeof unref === "object") {
return unref;
}
const args = unref.length === 0 ? [] : unref.split(_.DEL);
switch (p) {
case _.ARRAY:
this._dict.set(b, args);
for (let i = 0; i < args.length; i++) {
args[i] = this.decode(args[i]);
}
return args;
case _.OBJECT:
const obj = {};
this._dict.set(b, obj);
for (let i = 0; i < args.length; i += 2) {
obj[this._unref_str(args[i])] = this.decode(args[i + 1]);
}
return obj;
}
throw "unrecognized prefix";
}
}

View file

@ -1,220 +1,120 @@
import _ from "./chars.js"; import _ from "./chars.js";
// @ts-ignore
// @ts-ignore type HashFunction = (str: string) => string;
export default class Encoder { export default class Encoder {
// @ts-ignore _hash: HashFunction;
constructor(hash, slen = Infinity) { _slen: number;
// @ts-ignore _refmap: Map<any, any>;
_refset: Set<any>;
constructor(hash: HashFunction, slen = Infinity) {
this._hash = hash; this._hash = hash;
// @ts-ignore
this._slen = slen; this._slen = slen;
// @ts-ignore
this._refmap = new Map(); this._refmap = new Map();
// @ts-ignore
this._refset = new Set(); this._refset = new Set();
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
_ref_str(str) { _ref_str(str) {
// @ts-ignore if (str.length < this._slen && !str.includes(_.DEL)) {
if (str.length < this._slen && str.indexOf(_.DEL) === -1) {
// @ts-ignore
return str; return str;
// @ts-ignore
} }
// @ts-ignore
let ref = this._refmap.get(str); let ref = this._refmap.get(str);
// @ts-ignore
if (ref === undefined) { if (ref === undefined) {
// @ts-ignore
ref = this._hash(str); ref = this._hash(str);
// @ts-ignore
this._refmap.set(str, ref); this._refmap.set(str, ref);
// @ts-ignore
} }
// @ts-ignore
return ref; return ref;
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
_encode_prim(obj) { _encode_prim(obj) {
// @ts-ignore const type = typeof obj;
switch (typeof obj) { switch (type) {
// @ts-ignore
case "undefined": case "undefined":
// @ts-ignore
return _.UNDEF; return _.UNDEF;
// @ts-ignore
case "boolean": case "boolean":
// @ts-ignore
return obj ? _.TRUE : _.FALSE; return obj ? _.TRUE : _.FALSE;
// @ts-ignore
case "number": case "number":
// @ts-ignore
return _.NUMBER + obj.toString(); return _.NUMBER + obj.toString();
// @ts-ignore
case "bigint": case "bigint":
// @ts-ignore
return _.BIGINT + obj.toString(); return _.BIGINT + obj.toString();
// @ts-ignore
case "function": case "function":
// @ts-ignore
return _.FUNCTION; return _.FUNCTION;
// @ts-ignore
case "string": case "string":
// @ts-ignore
return _.STRING + this._ref_str(obj); return _.STRING + this._ref_str(obj);
// @ts-ignore
case "symbol": case "symbol":
// @ts-ignore
return _.SYMBOL + this._ref_str(obj.toString().slice(7, -1)); return _.SYMBOL + this._ref_str(obj.toString().slice(7, -1));
// @ts-ignore
} }
// @ts-ignore
if (obj === null) { if (obj === null) {
// @ts-ignore
return _.NULL; return _.NULL;
// @ts-ignore
} }
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
_encode_obj(obj, ref = this._refmap.get(obj)) { _encode_obj(obj, ref = this._refmap.get(obj)) {
// @ts-ignore
return (Array.isArray(obj) ? _.ARRAY : _.OBJECT) + ref; return (Array.isArray(obj) ? _.ARRAY : _.OBJECT) + ref;
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
_encode_term(obj) { _encode_term(obj) {
// @ts-ignore
return this._encode_prim(obj) || this._encode_obj(obj); return this._encode_prim(obj) || this._encode_obj(obj);
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
_encode_deep(obj, depth) { _encode_deep(obj, depth) {
// @ts-ignore
const enc = this._encode_prim(obj); const enc = this._encode_prim(obj);
// @ts-ignore
if (enc !== undefined) { if (enc !== undefined) {
// @ts-ignore
return enc; return enc;
// @ts-ignore
} }
// @ts-ignore
const ref = this._refmap.get(obj); const ref = this._refmap.get(obj);
// @ts-ignore
switch (typeof ref) { switch (typeof ref) {
// @ts-ignore
case "number": case "number":
// @ts-ignore
return (depth - ref).toString(); return (depth - ref).toString();
// @ts-ignore
case "string": case "string":
// @ts-ignore
return this._encode_obj(obj, ref); return this._encode_obj(obj, ref);
// @ts-ignore
} }
// @ts-ignore
this._refmap.set(obj, depth); this._refmap.set(obj, depth);
// @ts-ignore
const hash = this._hash( const hash = this._hash(
// @ts-ignore
(Array.isArray(obj) (Array.isArray(obj)
// @ts-ignore
? obj.map(v => this._encode_deep(v, depth + 1)) ? obj.map(v => this._encode_deep(v, depth + 1))
// @ts-ignore
: Object.keys(obj) : Object.keys(obj)
// @ts-ignore
.sort() .sort()
// @ts-ignore
.map( .map(
// @ts-ignore
k => k =>
// @ts-ignore
this._ref_str(k) + _.DEL + this._encode_deep(obj[k], depth + 1) this._ref_str(k) + _.DEL + this._encode_deep(obj[k], depth + 1)
// @ts-ignore
) )
// @ts-ignore
).join(_.DEL) ).join(_.DEL)
// @ts-ignore
); );
// @ts-ignore
this._refmap.set(obj, hash); this._refmap.set(obj, hash);
// @ts-ignore
return this._encode_obj(obj, hash); return this._encode_obj(obj, hash);
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
encode(obj) { encode(obj) {
// @ts-ignore
return this._encode_deep(obj, 0); return this._encode_deep(obj, 0);
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
commit() { commit() {
// @ts-ignore
const dict = {}; const dict = {};
// @ts-ignore
this._refmap.forEach((ref, obj) => { this._refmap.forEach((ref, obj) => {
// @ts-ignore
if (this._refset.has(ref)) { if (this._refset.has(ref)) {
// @ts-ignore
return; return;
// @ts-ignore
} }
// @ts-ignore
this._refset.add(ref); this._refset.add(ref);
// @ts-ignore
if (typeof obj !== "string") { if (typeof obj !== "string") {
// @ts-ignore
obj = (Array.isArray(obj) obj = (Array.isArray(obj)
// @ts-ignore
? obj.map(v => this._encode_term(v)) ? obj.map(v => this._encode_term(v))
// @ts-ignore
: Object.keys(obj).map( : Object.keys(obj).map(
// @ts-ignore
k => this._ref_str(k) + _.DEL + this._encode_term(obj[k]) k => this._ref_str(k) + _.DEL + this._encode_term(obj[k])
// @ts-ignore
) )
// @ts-ignore
).join(_.DEL); ).join(_.DEL);
// @ts-ignore
} }
// @ts-ignore
dict[ref] = obj; dict[ref] = obj;
// @ts-ignore
}); });
// @ts-ignore
this._refmap.clear(); this._refmap.clear();
// @ts-ignore
return dict; return dict;
// @ts-ignore
} }
// @ts-ignore
// @ts-ignore
clear() { clear() {
// @ts-ignore
this._refmap.clear(); this._refmap.clear();
// @ts-ignore
this._refset.clear(); this._refset.clear();
// @ts-ignore
} }
// @ts-ignore
} }
// @ts-ignore

View file

@ -0,0 +1,6 @@
import Encoder from "./encoder.js";
import Decoder from "./decoder.js";
import sha1 from "./sha1.js";
import murmur from "./mur.js";
export { Encoder, Decoder, sha1, murmur };

View file

@ -0,0 +1,162 @@
function murmurhash3_32_rp(key, seed) {
var keyLength, tailLength, tailLength4, bodyLength, bodyLength8, h1, k1, i, c1_low, c1_high, c2_low, c2_high, k1B, c3;
keyLength = key.length;
tailLength = keyLength & 3;
bodyLength = keyLength - tailLength;
tailLength4 = bodyLength & 7;
bodyLength8 = bodyLength - tailLength4;
h1 = seed;
//c1 = 0xcc9e2d51;
c1_low = 0x2d51;
c1_high = 0xcc9e0000;
//c2 = 0x1b873593;
c2_low = 0x3593;
c2_high = 0x1b870000;
c3 = 0xe6546b64;
//----------
// body
i = 0;
while (i < bodyLength8) {
k1 =
((key.charCodeAt(i) & 0xff)) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
k1B =
((key.charCodeAt(++i) & 0xff)) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
++i;
//k1 *= c1;
k1 = (c1_high * k1 | 0) + (c1_low * k1);
//k1 = ROTL32(k1,15);
k1 = (k1 << 15) | (k1 >>> 17);
//k1 *= c2;
k1 = (c2_high * k1 | 0) + (c2_low * k1);
//h1 ^= k1;
h1 ^= k1;
//h1 = ROTL32(h1,13);
h1 = (h1 << 13) | (h1 >>> 19);
//h1 = h1*5+0xe6546b64;
h1 = h1 * 5 + c3;
//k1 *= c1;
k1B = (c1_high * k1B | 0) + (c1_low * k1B);
//k1 = ROTL32(k1,15);
k1B = (k1B << 15) | (k1B >>> 17);
//k1 *= c2;
k1B = (c2_high * k1B | 0) + (c2_low * k1B);
//h1 ^= k1;
h1 ^= k1B;
//h1 = ROTL32(h1,13);
h1 = (h1 << 13) | (h1 >>> 19);
//h1 = h1*5+0xe6546b64;
h1 = h1 * 5 + c3;
} //while (i < bodyLength8) {
if (tailLength4) {
k1 =
((key.charCodeAt(i) & 0xff)) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
((key.charCodeAt(++i) & 0xff) << 24);
++i;
//k1 *= c1;
k1 = (c1_high * k1 | 0) + (c1_low * k1);
//k1 = ROTL32(k1,15);
k1 = (k1 << 15) | (k1 >>> 17);
//k1 *= c2;
k1 = (c2_high * k1 | 0) + (c2_low * k1);
//h1 ^= k1;
h1 ^= k1;
//h1 = ROTL32(h1,13);
h1 = (h1 << 13) | (h1 >>> 19);
//h1 = h1*5+0xe6546b64;
h1 = h1 * 5 + c3;
} //if (tailLength4) {
//----------
// tail
k1 = 0;
switch (tailLength) {
case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
case 1: k1 ^= (key.charCodeAt(i) & 0xff);
//k1 *= c1;
k1 = (c1_high * k1 | 0) + (c1_low * k1);
//k1 = ROTL32(k1,15);
k1 = (k1 << 15) | (k1 >>> 17);
//k1 *= c2;
k1 = (c2_high * k1 | 0) + (c2_low * k1);
//h1 ^= k1;
h1 ^= k1;
} //switch (tailLength) {
//----------
// finalization
h1 ^= keyLength;
//h1 = fmix32(h1);
{
//h ^= h >> 16;
h1 ^= h1 >>> 16;
//h1 *= 0x85ebca6b;
h1 = (0x85eb0000 * h1 | 0) + (0xca6b * h1);
//h ^= h >> 13;
h1 ^= h1 >>> 13;
//h1 *= 0xc2b2ae35;
h1 = (0xc2b20000 * h1 | 0) + (0xae35 * h1);
//h ^= h >> 16;
h1 ^= h1 >>> 16;
}
return h1 >>> 0; //convert to unsigned int
}
function murmurHash3ToBase64(key, seed = 0) {
let hashValue = murmurhash3_32_rp(key, seed);
return hashToBase64(hashValue);
}
function hashToBase64(hash) {
let buffer = new ArrayBuffer(4); // 32 bits for hash
let view = new DataView(buffer);
view.setUint32(0, hash, false); // Use big-endian
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
export default murmurHash3ToBase64;

View file

@ -0,0 +1,20 @@
declare type HashFunction = (str: string) => string;
declare type Dict = { [key: string]: string };
export class Encoder {
constructor(hash: HashFunction, slen?: number);
commit(): Dict;
encode(obj: any): string;
clear(): void;
}
export class Decoder {
constructor();
set(ref: string, enc: string): void;
assign(dict: Dict): void;
decode(enc: string): any;
clear(): void;
}
export const sha1: HashFunction;
export const murmur: HashFunction;

View file

@ -1,5 +0,0 @@
// TODO: SSR solution for all asayer libraries
import Encoder from "./encoder.js";
import sha1 from "./sha1.js";
export { Encoder, sha1 };

View file

@ -2,11 +2,12 @@
"compilerOptions": { "compilerOptions": {
"noImplicitThis": true, "noImplicitThis": true,
"strictNullChecks": true, "strictNullChecks": true,
"alwaysStrict": true, "alwaysStrict": false,
"target": "es6", "target": "es2017",
"module": "es6", "module": "es6",
"moduleResolution": "nodenext", "moduleResolution": "nodenext",
"declaration": true, "declaration": true,
"outDir": "./lib" "outDir": "./lib",
"lib": ["es2020", "dom"]
} }
} }