From 69a8f934bf1875c98169a9670c12f72056d31cce Mon Sep 17 00:00:00 2001 From: Ellyse Date: Wed, 16 Jul 2025 14:05:52 -0700 Subject: [PATCH 1/2] some fixes with NaN for keys but mostly logging --- .../runner/integration/array_push.test.ts | 27 ++++++---- .../runner/integration/array_push.test.tsx | 53 +++++++++++++++---- packages/runner/src/data-updating.ts | 23 +++++++- .../runner/src/storage/transaction-shim.ts | 38 ++++++++++++- 4 files changed, 117 insertions(+), 24 deletions(-) diff --git a/packages/runner/integration/array_push.test.ts b/packages/runner/integration/array_push.test.ts index e7a0a5d96..b57baab06 100644 --- a/packages/runner/integration/array_push.test.ts +++ b/packages/runner/integration/array_push.test.ts @@ -26,11 +26,12 @@ const MEMORY_WS_URL = `${ const SPACE_NAME = "runner_integration"; const TOTAL_COUNT = 100; // how many elements we push to the array -const TIMEOUT_MS = 30000; // timeout for the test in ms +const TIMEOUT_MS = 60000; // timeout for the test in ms console.log("Array Push Test"); console.log(`Connecting to: ${MEMORY_WS_URL}`); console.log(`Toolshed URL: ${TOOLSHED_URL}`); +console.log(`Space Name: ${SPACE_NAME}`); // Set up timeout const timeoutPromise = new Promise((_, reject) => { @@ -92,18 +93,22 @@ async function runTest() { console.log("Result charm ID:", getEntityId(charm)); // Wait so we can load the page on the browser - // await new Promise((resolve) => setTimeout(resolve, 10000)); + await new Promise((resolve) => setTimeout(resolve, 15000)); - // Get the handler stream and send some numbers + // BATCH VERSION - causes errors const pushHandlerStream = charm.key("pushHandler"); - let sendCount = 0; - - // Loop, sending numbers one by one - for (let i = 0; i < TOTAL_COUNT; i++) { - console.log(`Sending value: ${i}`); - pushHandlerStream.send({ value: i }); - sendCount++; - } + console.log("Sending event to push all items at once"); + pushHandlerStream.send({}); + + // ORIGINAL VERSION - works without errors + // const pushHandlerStream = charm.key("pushHandler"); + // let sendCount = 0; + // // Loop, sending numbers one by one + // for (let i = 0; i < TOTAL_COUNT; i++) { + // console.log(`Sending value: ${i}`); + // pushHandlerStream.send({ value: i }); + // sendCount++; + // } console.log("Waiting for runtime to finish and storage to sync..."); await runtime.idle(); diff --git a/packages/runner/integration/array_push.test.tsx b/packages/runner/integration/array_push.test.tsx index 8978f3e26..3e868854a 100644 --- a/packages/runner/integration/array_push.test.tsx +++ b/packages/runner/integration/array_push.test.tsx @@ -9,6 +9,8 @@ import { UI, } from "commontools"; +const ARRAY_LENGTH=1; + // dummy input schema const InputSchema = { type: "object", @@ -21,7 +23,13 @@ const OutputSchema = { properties: { my_array: { type: "array", - items: { type: "number" }, + items: { + type: "object", + properties: { + name: { type: "string" }, + value: { type: "number" }, + }, + }, default: [], asCell: true, }, @@ -34,14 +42,41 @@ export default recipe( InputSchema, OutputSchema, () => { - const my_array = cell([]); + const my_array = cell<{ name: string; value: number }[]>([]); - const pushHandler = handler( - ({ value }: { value: number }, { array }: { array: number[] }) => { - console.log("Pushing value:", value); - array.push(value); + // BATCH VERSION - causes "Path must not be empty" errors + const pushHandler = handler({}, { + type: "object", + properties: { + array: { + type: "array", + asCell: true, + items: { + type: "object", + properties: { name: { type: "string" }, value: { type: "number" } }, + }, + }, }, - ); + required: ["array"], + }, (_, { array }) => { + console.log("[pushHandler] Pushing all items at once"); + const itemsToAdd = Array.from({ length: ARRAY_LENGTH }, (_, i) => ({ + name: `Item ${i}`, + value: i, + })); + console.log("[pushHandler] Before push - array:", array); + console.log("[pushHandler] Items to add:", itemsToAdd); + array.push(...itemsToAdd); + // array.push(itemsToAdd[0]); + console.log("[pushHandler] After push - array.get():", array.get()); + }); + + // const pushHandler = handler( + // ({ value }: { value: number }, { array }: { array: { name: string; value: number }[] }) => { + // console.log("Pushing value:", value); + // array.push({ name: `Item ${value}`, value: value }); + // }, + // ); // Return the recipe return { @@ -52,9 +87,7 @@ export default recipe(

Array length: {derive(my_array, (arr) => arr.length)}

diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index 19b264ac0..e0524df7e 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -229,6 +229,10 @@ export function normalizeAndDiff( // Handle arrays if (Array.isArray(newValue)) { + console.log("[normalizeAndDiff] Array case - newValue:", newValue); + console.log("[normalizeAndDiff] Array case - currentValue:", currentValue); + console.log("[normalizeAndDiff] Array case - link:", link); + // If the current value is not an array, set it to an empty array if (!Array.isArray(currentValue)) { changes.push({ location: link, value: [] }); @@ -320,28 +324,43 @@ export function normalizeAndDiff( if ( link.path.length > 0 && link.path[link.path.length - 1] === "length" ) { + console.log("[normalizeAndDiff] Length property case"); + console.log("[normalizeAndDiff] link.path:", link.path); + console.log("[normalizeAndDiff] newValue:", newValue); + const maybeCurrentArray = tx.readValueOrThrow({ ...link, path: link.path.slice(0, -1), }); + console.log("[normalizeAndDiff] Parent array:", maybeCurrentArray); + if (Array.isArray(maybeCurrentArray)) { const currentLength = maybeCurrentArray.length; const newLength = newValue as number; + console.log("[normalizeAndDiff] currentLength:", currentLength); + console.log("[normalizeAndDiff] newLength:", newLength); + if (currentLength !== newLength) { changes.push({ location: link, value: newLength }); + console.log("[normalizeAndDiff] Array length changed, need to update elements"); + console.log("[normalizeAndDiff] Loop from", Math.min(currentLength, newLength), "to", Math.max(currentLength, newLength)); + for ( let i = Math.min(currentLength, newLength); i < Math.max(currentLength, newLength); i++ ) { + console.log("[normalizeAndDiff] Adding undefined at path:", [...link.path.slice(0, -1), i.toString()]); changes.push({ - location: { ...link, path: [...link.path, i.toString()] }, + location: { ...link, path: [...link.path.slice(0, -1), i.toString()] }, value: undefined, }); } return changes; } - } // else, i.e. parent is not an array: fall through to the primitive case + } else { + console.log("[normalizeAndDiff] Parent is not an array, falling through"); + } } // Handle primitive values and other cases (Object.is handles NaN and -0) diff --git a/packages/runner/src/storage/transaction-shim.ts b/packages/runner/src/storage/transaction-shim.ts index 71264c106..4848044ae 100644 --- a/packages/runner/src/storage/transaction-shim.ts +++ b/packages/runner/src/storage/transaction-shim.ts @@ -205,6 +205,7 @@ class TransactionReader implements ITransactionReader { // Path-based logic if (!address.path.length) { + console.error("Path must not be empty for address:", address); const notFoundError: INotFoundError = new Error( `Path must not be empty`, ) as INotFoundError; @@ -310,6 +311,20 @@ class TransactionWriter extends TransactionReader throw new Error(`Failed to get or create document: ${address.id}`); } + if (!address.path.length) { + console.error("Path must not be empty for address:", address); + console.error("Value: ", value); + console.error("value is object:", isObject(value)); + console.error("'value' in value:", "value" in value); + console.error("value.value:", value?.value); + } + + // Rewrite creating new documents as setting the value + if (address.path.length === 0 && isObject(value) && "value" in value) { + address = { ...address, path: ["value"] }; + value = value.value; + } + // Path-based logic if (!address.path.length) { const notFoundError: INotFoundError = new Error( @@ -565,31 +580,48 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction { ): void { const writeResult = this.tx.write(address, value); if (writeResult.error && writeResult.error.name === "NotFoundError") { + console.log("[writeOrThrow] NotFoundError retry logic"); + console.log("[writeOrThrow] address:", address); + console.log("[writeOrThrow] value:", value); + console.log("[writeOrThrow] error.path:", writeResult.error.path); + // Create parent entries if needed const lastValidPath = writeResult.error.path; const valueObj = lastValidPath ? this.readValueOrThrow({ ...address, path: lastValidPath }) : {}; + console.log("[writeOrThrow] valueObj after read:", valueObj); + if (!isRecord(valueObj)) { throw new Error( `Value at path ${address.path.join("/")} is not an object`, ); } const remainingPath = address.path.slice(lastValidPath?.length ?? 0); + console.log("[writeOrThrow] remainingPath:", remainingPath); + if (remainingPath.length === 0) { throw new Error( `Invalid error path: ${lastValidPath?.join("/")}`, ); } const lastKey = remainingPath.pop()!; + console.log("[writeOrThrow] lastKey:", lastKey); + let nextValue = valueObj; for (const key of remainingPath) { + console.log("[writeOrThrow] Creating structure for key:", key); + console.log("[writeOrThrow] typeof Number(key):", typeof Number(key)); + console.log("[writeOrThrow] Number.isNaN(Number(key)):", Number.isNaN(Number(key))); nextValue = nextValue[key] = - (typeof Number(key) === "number" ? [] : {}) as typeof nextValue; + (!Number.isNaN(Number(key)) ? [] : {}) as typeof nextValue; } nextValue[lastKey] = value; + console.log("[writeOrThrow] Final valueObj to write:", valueObj); + const parentAddress = { ...address, path: lastValidPath ?? [] }; + console.log("[writeOrThrow] parentAddress:", parentAddress); const writeResultRetry = this.tx.write(parentAddress, valueObj); if (writeResultRetry.error) { throw writeResultRetry.error; @@ -603,6 +635,10 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction { address: IMemorySpaceAddress, value: JSONValue | undefined, ): void { + console.log("[writeValueOrThrow] Input address:", address); + console.log("[writeValueOrThrow] Input path:", address.path); + console.log("[writeValueOrThrow] Input value:", value); + console.log("[writeValueOrThrow] Transformed path:", ["value", ...address.path]); this.writeOrThrow({ ...address, path: ["value", ...address.path] }, value); } From 62f09ca9922ac0a1594303e32edd8214ff8ddaf6 Mon Sep 17 00:00:00 2001 From: Ellyse Date: Wed, 16 Jul 2025 14:33:16 -0700 Subject: [PATCH 2/2] fixed the path generation when setting array length to 0, fixed array detection in retry logic, cherrypick bernis change for handling for root writes with value property --- .../runner/integration/array_push.test.ts | 27 ++++------ .../runner/integration/array_push.test.tsx | 53 ++++--------------- packages/runner/src/data-updating.ts | 21 +------- .../runner/src/storage/transaction-shim.ts | 30 ----------- 4 files changed, 22 insertions(+), 109 deletions(-) diff --git a/packages/runner/integration/array_push.test.ts b/packages/runner/integration/array_push.test.ts index b57baab06..e7a0a5d96 100644 --- a/packages/runner/integration/array_push.test.ts +++ b/packages/runner/integration/array_push.test.ts @@ -26,12 +26,11 @@ const MEMORY_WS_URL = `${ const SPACE_NAME = "runner_integration"; const TOTAL_COUNT = 100; // how many elements we push to the array -const TIMEOUT_MS = 60000; // timeout for the test in ms +const TIMEOUT_MS = 30000; // timeout for the test in ms console.log("Array Push Test"); console.log(`Connecting to: ${MEMORY_WS_URL}`); console.log(`Toolshed URL: ${TOOLSHED_URL}`); -console.log(`Space Name: ${SPACE_NAME}`); // Set up timeout const timeoutPromise = new Promise((_, reject) => { @@ -93,22 +92,18 @@ async function runTest() { console.log("Result charm ID:", getEntityId(charm)); // Wait so we can load the page on the browser - await new Promise((resolve) => setTimeout(resolve, 15000)); + // await new Promise((resolve) => setTimeout(resolve, 10000)); - // BATCH VERSION - causes errors + // Get the handler stream and send some numbers const pushHandlerStream = charm.key("pushHandler"); - console.log("Sending event to push all items at once"); - pushHandlerStream.send({}); - - // ORIGINAL VERSION - works without errors - // const pushHandlerStream = charm.key("pushHandler"); - // let sendCount = 0; - // // Loop, sending numbers one by one - // for (let i = 0; i < TOTAL_COUNT; i++) { - // console.log(`Sending value: ${i}`); - // pushHandlerStream.send({ value: i }); - // sendCount++; - // } + let sendCount = 0; + + // Loop, sending numbers one by one + for (let i = 0; i < TOTAL_COUNT; i++) { + console.log(`Sending value: ${i}`); + pushHandlerStream.send({ value: i }); + sendCount++; + } console.log("Waiting for runtime to finish and storage to sync..."); await runtime.idle(); diff --git a/packages/runner/integration/array_push.test.tsx b/packages/runner/integration/array_push.test.tsx index 3e868854a..8978f3e26 100644 --- a/packages/runner/integration/array_push.test.tsx +++ b/packages/runner/integration/array_push.test.tsx @@ -9,8 +9,6 @@ import { UI, } from "commontools"; -const ARRAY_LENGTH=1; - // dummy input schema const InputSchema = { type: "object", @@ -23,13 +21,7 @@ const OutputSchema = { properties: { my_array: { type: "array", - items: { - type: "object", - properties: { - name: { type: "string" }, - value: { type: "number" }, - }, - }, + items: { type: "number" }, default: [], asCell: true, }, @@ -42,41 +34,14 @@ export default recipe( InputSchema, OutputSchema, () => { - const my_array = cell<{ name: string; value: number }[]>([]); + const my_array = cell([]); - // BATCH VERSION - causes "Path must not be empty" errors - const pushHandler = handler({}, { - type: "object", - properties: { - array: { - type: "array", - asCell: true, - items: { - type: "object", - properties: { name: { type: "string" }, value: { type: "number" } }, - }, - }, + const pushHandler = handler( + ({ value }: { value: number }, { array }: { array: number[] }) => { + console.log("Pushing value:", value); + array.push(value); }, - required: ["array"], - }, (_, { array }) => { - console.log("[pushHandler] Pushing all items at once"); - const itemsToAdd = Array.from({ length: ARRAY_LENGTH }, (_, i) => ({ - name: `Item ${i}`, - value: i, - })); - console.log("[pushHandler] Before push - array:", array); - console.log("[pushHandler] Items to add:", itemsToAdd); - array.push(...itemsToAdd); - // array.push(itemsToAdd[0]); - console.log("[pushHandler] After push - array.get():", array.get()); - }); - - // const pushHandler = handler( - // ({ value }: { value: number }, { array }: { array: { name: string; value: number }[] }) => { - // console.log("Pushing value:", value); - // array.push({ name: `Item ${value}`, value: value }); - // }, - // ); + ); // Return the recipe return { @@ -87,7 +52,9 @@ export default recipe(

Array length: {derive(my_array, (arr) => arr.length)}

    - Current values: {my_array.map((e) =>
  • {e.name}
  • )} + Current values: {my_array.map((e) => ( +
  • {e}
  • + ))}

diff --git a/packages/runner/src/data-updating.ts b/packages/runner/src/data-updating.ts index e0524df7e..c457a15fc 100644 --- a/packages/runner/src/data-updating.ts +++ b/packages/runner/src/data-updating.ts @@ -229,10 +229,6 @@ export function normalizeAndDiff( // Handle arrays if (Array.isArray(newValue)) { - console.log("[normalizeAndDiff] Array case - newValue:", newValue); - console.log("[normalizeAndDiff] Array case - currentValue:", currentValue); - console.log("[normalizeAndDiff] Array case - link:", link); - // If the current value is not an array, set it to an empty array if (!Array.isArray(currentValue)) { changes.push({ location: link, value: [] }); @@ -324,33 +320,20 @@ export function normalizeAndDiff( if ( link.path.length > 0 && link.path[link.path.length - 1] === "length" ) { - console.log("[normalizeAndDiff] Length property case"); - console.log("[normalizeAndDiff] link.path:", link.path); - console.log("[normalizeAndDiff] newValue:", newValue); - const maybeCurrentArray = tx.readValueOrThrow({ ...link, path: link.path.slice(0, -1), }); - console.log("[normalizeAndDiff] Parent array:", maybeCurrentArray); - if (Array.isArray(maybeCurrentArray)) { const currentLength = maybeCurrentArray.length; const newLength = newValue as number; - console.log("[normalizeAndDiff] currentLength:", currentLength); - console.log("[normalizeAndDiff] newLength:", newLength); - if (currentLength !== newLength) { changes.push({ location: link, value: newLength }); - console.log("[normalizeAndDiff] Array length changed, need to update elements"); - console.log("[normalizeAndDiff] Loop from", Math.min(currentLength, newLength), "to", Math.max(currentLength, newLength)); - for ( let i = Math.min(currentLength, newLength); i < Math.max(currentLength, newLength); i++ ) { - console.log("[normalizeAndDiff] Adding undefined at path:", [...link.path.slice(0, -1), i.toString()]); changes.push({ location: { ...link, path: [...link.path.slice(0, -1), i.toString()] }, value: undefined, @@ -358,9 +341,7 @@ export function normalizeAndDiff( } return changes; } - } else { - console.log("[normalizeAndDiff] Parent is not an array, falling through"); - } + } // else, i.e. parent is not an array: fall through to the primitive case } // Handle primitive values and other cases (Object.is handles NaN and -0) diff --git a/packages/runner/src/storage/transaction-shim.ts b/packages/runner/src/storage/transaction-shim.ts index 4848044ae..fffc246b1 100644 --- a/packages/runner/src/storage/transaction-shim.ts +++ b/packages/runner/src/storage/transaction-shim.ts @@ -205,7 +205,6 @@ class TransactionReader implements ITransactionReader { // Path-based logic if (!address.path.length) { - console.error("Path must not be empty for address:", address); const notFoundError: INotFoundError = new Error( `Path must not be empty`, ) as INotFoundError; @@ -311,14 +310,6 @@ class TransactionWriter extends TransactionReader throw new Error(`Failed to get or create document: ${address.id}`); } - if (!address.path.length) { - console.error("Path must not be empty for address:", address); - console.error("Value: ", value); - console.error("value is object:", isObject(value)); - console.error("'value' in value:", "value" in value); - console.error("value.value:", value?.value); - } - // Rewrite creating new documents as setting the value if (address.path.length === 0 && isObject(value) && "value" in value) { address = { ...address, path: ["value"] }; @@ -580,48 +571,31 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction { ): void { const writeResult = this.tx.write(address, value); if (writeResult.error && writeResult.error.name === "NotFoundError") { - console.log("[writeOrThrow] NotFoundError retry logic"); - console.log("[writeOrThrow] address:", address); - console.log("[writeOrThrow] value:", value); - console.log("[writeOrThrow] error.path:", writeResult.error.path); - // Create parent entries if needed const lastValidPath = writeResult.error.path; const valueObj = lastValidPath ? this.readValueOrThrow({ ...address, path: lastValidPath }) : {}; - console.log("[writeOrThrow] valueObj after read:", valueObj); - if (!isRecord(valueObj)) { throw new Error( `Value at path ${address.path.join("/")} is not an object`, ); } const remainingPath = address.path.slice(lastValidPath?.length ?? 0); - console.log("[writeOrThrow] remainingPath:", remainingPath); - if (remainingPath.length === 0) { throw new Error( `Invalid error path: ${lastValidPath?.join("/")}`, ); } const lastKey = remainingPath.pop()!; - console.log("[writeOrThrow] lastKey:", lastKey); - let nextValue = valueObj; for (const key of remainingPath) { - console.log("[writeOrThrow] Creating structure for key:", key); - console.log("[writeOrThrow] typeof Number(key):", typeof Number(key)); - console.log("[writeOrThrow] Number.isNaN(Number(key)):", Number.isNaN(Number(key))); nextValue = nextValue[key] = (!Number.isNaN(Number(key)) ? [] : {}) as typeof nextValue; } nextValue[lastKey] = value; - console.log("[writeOrThrow] Final valueObj to write:", valueObj); - const parentAddress = { ...address, path: lastValidPath ?? [] }; - console.log("[writeOrThrow] parentAddress:", parentAddress); const writeResultRetry = this.tx.write(parentAddress, valueObj); if (writeResultRetry.error) { throw writeResultRetry.error; @@ -635,10 +609,6 @@ export class ExtendedStorageTransaction implements IExtendedStorageTransaction { address: IMemorySpaceAddress, value: JSONValue | undefined, ): void { - console.log("[writeValueOrThrow] Input address:", address); - console.log("[writeValueOrThrow] Input path:", address.path); - console.log("[writeValueOrThrow] Input value:", value); - console.log("[writeValueOrThrow] Transformed path:", ["value", ...address.path]); this.writeOrThrow({ ...address, path: ["value", ...address.path] }, value); }