@voidhash/mimic-react 0.0.1-alpha.5 → 0.0.1-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @voidhash/mimic-react@0.0.1-alpha.5 build /home/runner/work/mimic/mimic/packages/mimic-react
2
+ > @voidhash/mimic-react@0.0.1-alpha.6 build /home/runner/work/mimic/mimic/packages/mimic-react
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.18.2 powered by rolldown v1.0.0-beta.55
@@ -13,7 +13,7 @@
13
13
  ℹ [ESM] dist/index.mjs 0.01 kB │ gzip: 0.03 kB
14
14
  ℹ [ESM] dist/index.d.mts 0.01 kB │ gzip: 0.03 kB
15
15
  ℹ [ESM] 2 files, total: 0.02 kB
16
+ ✔ Build complete in 3369ms
16
17
  ℹ [CJS] dist/index.d.cts 0.01 kB │ gzip: 0.03 kB
17
18
  ℹ [CJS] 1 files, total: 0.01 kB
18
- ✔ Build complete in 2911ms
19
- ✔ Build complete in 2911ms
19
+ ✔ Build complete in 3371ms
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidhash/mimic-react",
3
- "version": "0.0.1-alpha.5",
3
+ "version": "0.0.1-alpha.6",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,21 +9,27 @@
9
9
  },
10
10
  "main": "./src/index.ts",
11
11
  "exports": {
12
- "./zustand": "./src/zustand/index.ts"
12
+ "./zustand": "./src/zustand/index.ts",
13
+ "./zustand-commander": "./src/zustand-commander/index.ts"
13
14
  },
14
15
  "dependencies": {
15
16
  "zustand": "^5.0.9"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@effect/vitest": "^0.26.0",
20
+ "@types/react": "^19.0.0",
21
+ "effect": "^3.16.0",
22
+ "react": "^19.0.0",
19
23
  "tsdown": "^0.18.2",
20
24
  "typescript": "5.8.3",
21
25
  "vite-tsconfig-paths": "^5.1.4",
22
26
  "vitest": "^3.2.4",
23
- "@voidhash/tsconfig": "0.0.1-alpha.5"
27
+ "@voidhash/tsconfig": "0.0.1-alpha.6"
24
28
  },
25
29
  "peerDependencies": {
26
- "@voidhash/mimic": "0.0.1-alpha.5"
30
+ "effect": "^3.16.0",
31
+ "react": "^18.0.0 || ^19.0.0",
32
+ "@voidhash/mimic": "0.0.1-alpha.6"
27
33
  },
28
34
  "scripts": {
29
35
  "build": "tsdown",
@@ -70,7 +70,7 @@ const createMimicObject = <
70
70
  const mimicImpl: MimicMiddlewareImpl = <
71
71
  TSchema extends Primitive.AnyPrimitive,
72
72
  TPresence extends Presence.AnyPresence | undefined = undefined,
73
- T extends object = object
73
+ _T extends object = object
74
74
  >(
75
75
  document: ClientDocument.ClientDocument<TSchema, TPresence>,
76
76
  config: any,
@@ -0,0 +1,395 @@
1
+ /**
2
+ * @voidhash/mimic-react/zustand-commander
3
+ *
4
+ * Commander creation and command definition.
5
+ *
6
+ * @since 0.0.1
7
+ */
8
+
9
+ import type { StoreApi } from "zustand";
10
+ import {
11
+ COMMAND_SYMBOL,
12
+ UNDOABLE_COMMAND_SYMBOL,
13
+ isUndoableCommand,
14
+ type AnyEffectSchema,
15
+ type Command,
16
+ type Commander,
17
+ type CommanderOptions,
18
+ type CommanderSlice,
19
+ type CommandContext,
20
+ type CommandDispatch,
21
+ type CommandFn,
22
+ type InferSchemaType,
23
+ type RevertFn,
24
+ type UndoableCommand,
25
+ type UndoEntry,
26
+ } from "./types.js";
27
+
28
+ // =============================================================================
29
+ // Default Options
30
+ // =============================================================================
31
+
32
+ const DEFAULT_OPTIONS: Required<CommanderOptions> = {
33
+ maxUndoStackSize: 100,
34
+ };
35
+
36
+ // =============================================================================
37
+ // Commander Implementation
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Creates a commander instance bound to a specific store type.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // Create commander for your store type
46
+ * const commander = createCommander<StoreState>();
47
+ *
48
+ * // Define commands
49
+ * const addItem = commander.action(
50
+ * Schema.Struct({ name: Schema.String }),
51
+ * (ctx, params) => {
52
+ * const { mimic } = ctx.getState();
53
+ * mimic.document.transaction(root => {
54
+ * // add item
55
+ * });
56
+ * }
57
+ * );
58
+ *
59
+ * // Create store with middleware
60
+ * const useStore = create(
61
+ * commander.middleware(
62
+ * mimic(document, (set, get) => ({
63
+ * // your state
64
+ * }))
65
+ * )
66
+ * );
67
+ * ```
68
+ */
69
+ export function createCommander<TStore extends object>(
70
+ options: CommanderOptions = {}
71
+ ): Commander<TStore & CommanderSlice> {
72
+ const { maxUndoStackSize } = { ...DEFAULT_OPTIONS, ...options };
73
+
74
+ // Track the store API once middleware is applied
75
+ let _storeApi: StoreApi<TStore & CommanderSlice> | null = null;
76
+
77
+ /**
78
+ * Creates the dispatch function for use within command handlers.
79
+ */
80
+ const createDispatch = (): CommandDispatch<TStore & CommanderSlice> => {
81
+ return <TParams, TReturn>(
82
+ command: Command<TStore & CommanderSlice, TParams, TReturn>
83
+ ) => {
84
+ return (params: TParams): TReturn => {
85
+ if (!_storeApi) {
86
+ throw new Error(
87
+ "Commander: Store not initialized. Make sure to use the commander middleware."
88
+ );
89
+ }
90
+
91
+ // Create context for the command
92
+ const ctx: CommandContext<TStore & CommanderSlice> = {
93
+ getState: () => _storeApi!.getState(),
94
+ setState: (partial) => _storeApi!.setState(partial as any),
95
+ dispatch: createDispatch(),
96
+ };
97
+
98
+ // Execute the command
99
+ const result = command.fn(ctx, params);
100
+
101
+ // If it's an undoable command, add to undo stack
102
+ if (isUndoableCommand(command)) {
103
+ const entry: UndoEntry<TParams, TReturn> = {
104
+ command,
105
+ params,
106
+ result,
107
+ timestamp: Date.now(),
108
+ };
109
+
110
+ _storeApi.setState((state: TStore & CommanderSlice) => {
111
+ const { undoStack, redoStack } = state._commander;
112
+
113
+ // Add to undo stack, respecting max size
114
+ const newUndoStack = [...undoStack, entry].slice(-maxUndoStackSize);
115
+
116
+ // Clear redo stack when a new command is executed
117
+ return {
118
+ ...state,
119
+ _commander: {
120
+ undoStack: newUndoStack,
121
+ redoStack: [],
122
+ },
123
+ };
124
+ });
125
+ }
126
+
127
+ return result;
128
+ };
129
+ };
130
+ };
131
+
132
+ /**
133
+ * Create a regular command (no undo support).
134
+ */
135
+ function action<TParamsSchema extends AnyEffectSchema, TReturn = void>(
136
+ paramsSchema: TParamsSchema,
137
+ fn: CommandFn<TStore & CommanderSlice, InferSchemaType<TParamsSchema>, TReturn>
138
+ ): Command<TStore & CommanderSlice, InferSchemaType<TParamsSchema>, TReturn>;
139
+ function action<TReturn = void>(
140
+ fn: CommandFn<TStore & CommanderSlice, void, TReturn>
141
+ ): Command<TStore & CommanderSlice, void, TReturn>;
142
+ function action<TParamsSchema extends AnyEffectSchema, TReturn = void>(
143
+ paramsSchemaOrFn:
144
+ | TParamsSchema
145
+ | CommandFn<TStore & CommanderSlice, void, TReturn>,
146
+ maybeFn?: CommandFn<
147
+ TStore & CommanderSlice,
148
+ InferSchemaType<TParamsSchema>,
149
+ TReturn
150
+ >
151
+ ): Command<TStore & CommanderSlice, any, TReturn> {
152
+ // Check if we have two arguments (schema + fn) or just one (fn only)
153
+ if (maybeFn !== undefined) {
154
+ // First arg is schema, second is fn
155
+ return {
156
+ [COMMAND_SYMBOL]: true,
157
+ fn: maybeFn,
158
+ paramsSchema: paramsSchemaOrFn as TParamsSchema,
159
+ };
160
+ }
161
+
162
+ // Single argument - must be the action function (no params schema)
163
+ if (typeof paramsSchemaOrFn !== "function") {
164
+ throw new Error("Commander: action requires a function");
165
+ }
166
+
167
+ return {
168
+ [COMMAND_SYMBOL]: true,
169
+ fn: paramsSchemaOrFn as CommandFn<TStore & CommanderSlice, void, TReturn>,
170
+ paramsSchema: null,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Create an undoable command with undo/redo support.
176
+ */
177
+ function undoableAction<TParamsSchema extends AnyEffectSchema, TReturn>(
178
+ paramsSchema: TParamsSchema,
179
+ fn: CommandFn<TStore & CommanderSlice, InferSchemaType<TParamsSchema>, TReturn>,
180
+ revert: RevertFn<TStore & CommanderSlice, InferSchemaType<TParamsSchema>, TReturn>
181
+ ): UndoableCommand<TStore & CommanderSlice, InferSchemaType<TParamsSchema>, TReturn>;
182
+ function undoableAction<TReturn>(
183
+ fn: CommandFn<TStore & CommanderSlice, void, TReturn>,
184
+ revert: RevertFn<TStore & CommanderSlice, void, TReturn>
185
+ ): UndoableCommand<TStore & CommanderSlice, void, TReturn>;
186
+ function undoableAction<TParamsSchema extends AnyEffectSchema, TReturn>(
187
+ paramsSchemaOrFn:
188
+ | TParamsSchema
189
+ | CommandFn<TStore & CommanderSlice, void, TReturn>,
190
+ fnOrRevert:
191
+ | CommandFn<TStore & CommanderSlice, InferSchemaType<TParamsSchema>, TReturn>
192
+ | RevertFn<TStore & CommanderSlice, void, TReturn>,
193
+ maybeRevert?: RevertFn<
194
+ TStore & CommanderSlice,
195
+ InferSchemaType<TParamsSchema>,
196
+ TReturn
197
+ >
198
+ ): UndoableCommand<TStore & CommanderSlice, any, TReturn> {
199
+ // Check if we have three arguments (schema + fn + revert) or two (fn + revert)
200
+ if (maybeRevert !== undefined) {
201
+ // First arg is schema, second is fn, third is revert
202
+ return {
203
+ [COMMAND_SYMBOL]: true,
204
+ [UNDOABLE_COMMAND_SYMBOL]: true,
205
+ fn: fnOrRevert as CommandFn<
206
+ TStore & CommanderSlice,
207
+ InferSchemaType<TParamsSchema>,
208
+ TReturn
209
+ >,
210
+ paramsSchema: paramsSchemaOrFn as TParamsSchema,
211
+ revert: maybeRevert,
212
+ };
213
+ }
214
+
215
+ // Two arguments - fn + revert (no params schema)
216
+ if (typeof paramsSchemaOrFn !== "function") {
217
+ throw new Error("Commander: undoableAction requires a function");
218
+ }
219
+
220
+ return {
221
+ [COMMAND_SYMBOL]: true,
222
+ [UNDOABLE_COMMAND_SYMBOL]: true,
223
+ fn: paramsSchemaOrFn as CommandFn<TStore & CommanderSlice, void, TReturn>,
224
+ paramsSchema: null,
225
+ revert: fnOrRevert as RevertFn<TStore & CommanderSlice, void, TReturn>,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Zustand middleware that adds commander functionality.
231
+ */
232
+ const middleware = <T extends object>(
233
+ config: (
234
+ set: StoreApi<T & CommanderSlice>["setState"],
235
+ get: StoreApi<T & CommanderSlice>["getState"],
236
+ api: StoreApi<T & CommanderSlice>
237
+ ) => T
238
+ ) => {
239
+ return (
240
+ set: StoreApi<T & CommanderSlice>["setState"],
241
+ get: StoreApi<T & CommanderSlice>["getState"],
242
+ api: StoreApi<T & CommanderSlice>
243
+ ): T & CommanderSlice => {
244
+ // Store the API reference for dispatch
245
+ _storeApi = api as unknown as StoreApi<TStore & CommanderSlice>;
246
+
247
+ // Get user's state
248
+ const userState = config(set, get, api);
249
+
250
+ // Add commander slice
251
+ return {
252
+ ...userState,
253
+ _commander: {
254
+ undoStack: [],
255
+ redoStack: [],
256
+ },
257
+ };
258
+ };
259
+ };
260
+
261
+ return {
262
+ action,
263
+ undoableAction,
264
+ middleware: middleware as Commander<TStore & CommanderSlice>["middleware"],
265
+ };
266
+ }
267
+
268
+ // =============================================================================
269
+ // Undo/Redo Functions
270
+ // =============================================================================
271
+
272
+ /**
273
+ * Perform an undo operation on the store.
274
+ * Returns true if an undo was performed, false if undo stack was empty.
275
+ */
276
+ export function performUndo<TStore extends CommanderSlice>(
277
+ storeApi: StoreApi<TStore>
278
+ ): boolean {
279
+ const state = storeApi.getState();
280
+ const { undoStack, redoStack } = state._commander;
281
+
282
+ // Pop the last entry from undo stack
283
+ const entry = undoStack[undoStack.length - 1];
284
+ if (!entry) {
285
+ return false;
286
+ }
287
+
288
+ const newUndoStack = undoStack.slice(0, -1);
289
+
290
+ // Create context for the revert function
291
+ const ctx: CommandContext<TStore> = {
292
+ getState: () => storeApi.getState(),
293
+ setState: (partial) => storeApi.setState(partial as any),
294
+ dispatch: createDispatchForUndo(storeApi),
295
+ };
296
+
297
+ // Execute the revert function
298
+ entry.command.revert(ctx, entry.params, entry.result);
299
+
300
+ // Move entry to redo stack
301
+ storeApi.setState((state: TStore) => ({
302
+ ...state,
303
+ _commander: {
304
+ undoStack: newUndoStack,
305
+ redoStack: [...redoStack, entry],
306
+ },
307
+ }));
308
+
309
+ return true;
310
+ }
311
+
312
+ /**
313
+ * Perform a redo operation on the store.
314
+ * Returns true if a redo was performed, false if redo stack was empty.
315
+ */
316
+ export function performRedo<TStore extends CommanderSlice>(
317
+ storeApi: StoreApi<TStore>
318
+ ): boolean {
319
+ const state = storeApi.getState();
320
+ const { undoStack, redoStack } = state._commander;
321
+
322
+ // Pop the last entry from redo stack
323
+ const entry = redoStack[redoStack.length - 1];
324
+ if (!entry) {
325
+ return false;
326
+ }
327
+
328
+ const newRedoStack = redoStack.slice(0, -1);
329
+
330
+ // Create context for re-executing the command
331
+ const ctx: CommandContext<TStore> = {
332
+ getState: () => storeApi.getState(),
333
+ setState: (partial) => storeApi.setState(partial as any),
334
+ dispatch: createDispatchForUndo(storeApi),
335
+ };
336
+
337
+ // Re-execute the command
338
+ const result = entry.command.fn(ctx, entry.params);
339
+
340
+ // Create new entry with potentially new result
341
+ const newEntry: UndoEntry = {
342
+ command: entry.command,
343
+ params: entry.params,
344
+ result,
345
+ timestamp: Date.now(),
346
+ };
347
+
348
+ // Move entry back to undo stack
349
+ storeApi.setState((state: TStore) => ({
350
+ ...state,
351
+ _commander: {
352
+ undoStack: [...undoStack, newEntry],
353
+ redoStack: newRedoStack,
354
+ },
355
+ }));
356
+
357
+ return true;
358
+ }
359
+
360
+ /**
361
+ * Creates a dispatch function for use during undo/redo operations.
362
+ * This dispatch does NOT add to undo stack (to avoid infinite loops).
363
+ */
364
+ function createDispatchForUndo<TStore>(
365
+ storeApi: StoreApi<TStore>
366
+ ): CommandDispatch<TStore> {
367
+ return <TParams, TReturn>(command: Command<TStore, TParams, TReturn>) => {
368
+ return (params: TParams): TReturn => {
369
+ const ctx: CommandContext<TStore> = {
370
+ getState: () => storeApi.getState(),
371
+ setState: (partial) => storeApi.setState(partial as any),
372
+ dispatch: createDispatchForUndo(storeApi),
373
+ };
374
+
375
+ // Execute without adding to undo stack
376
+ return command.fn(ctx, params);
377
+ };
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Clear the undo and redo stacks.
383
+ */
384
+ export function clearUndoHistory<TStore extends CommanderSlice>(
385
+ storeApi: StoreApi<TStore>
386
+ ): void {
387
+ storeApi.setState((state: TStore) => ({
388
+ ...state,
389
+ _commander: {
390
+ undoStack: [],
391
+ redoStack: [],
392
+ },
393
+ }));
394
+ }
395
+