client

Documentation for using streamListDiff in the browser.

IMPORT

import { streamListDiff } from "@donedeal0/superdiff/client";

streamListDiff requires ESM support for browser usage. It will work out of the box if you use a modern bundler (Webpack, Rollup) or JavaScript framework (Next.js, Vue.js).


FORMAT

Input

In a browser environment, ReadableStream refers to the browser's streaming API, and File refers to an uploaded or local file. Examples are provided in the #usage section below.

 prevList: ReadableStream<Record<string, unknown>> | File | Record<string, unknown>[],
 nextList: ReadableStream<Record<string, unknown>> | File | Record<string, unknown>[],
 referenceProperty: keyof Record<string, unknown>,
 options: {
  showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
  chunksSize?: number, // 0 by default
  considerMoveAsUpdate?: boolean; // false by default
  useWorker?: boolean; // true by default
  showWarnings?: boolean; // true by default

}
  • prevList: the original object list.

  • nextList: the new object list.

  • referenceProperty: a property common to all objects in your lists (e.g. id).

  • options

    • chunksSize the number of object diffs returned by each streamed chunk. (e.g. 0 = 1 object diff per chunk, 10 = 10 object diffs per chunk).

    • showOnly gives you the option to return only the values whose status you are interested in (e.g. ["added", "equal"]).

    • considerMoveAsUpdate: if set to true a moved value will be considered as updated.

    • useWorker: if set to true, the diff will be run in a worker for maximum performance. Only recommended for large lists (e.g. +100,000 items).

    • showWarnings: if set to true, potential warnings will be displayed in the console.

Output

The objects diff are grouped into arrays - called chunks - and are consumed thanks to an event listener. You have access to 3 events:

  • data: to be notified when a new chunk of object diffs is available.

  • finish: to be notified when the stream is finished.

  • error: to be notified if an error occurs during the stream.

interface StreamListener<T> {
  on(event: "data", listener: (chunk: StreamListDiff<T>[]) => void);
  on(event: "finish", listener: () => void);
  on(event: "error", listener: (error: Error) => void);
}

type StreamListDiff<T extends Record<string, unknown>> = {
  currentValue: T | null;
  previousValue: T | null;
  prevIndex: number | null;
  newIndex: number | null;
  indexDiff: number | null;
  status: "added" | "deleted" | "moved" | "updated" | "equal";
};

USAGE

Input

Array


const diff = streamListDiff(
      [ 
        { id: 1, name: "Item 1" },  
        { id: 2, name: "Item 2" },
        { id: 3, name: "Item 3" } 
      ],
      [
        { id: 0, name: "Item 0" }, 
        { id: 2, name: "Item 2" },
        { id: 3, name: "Item Three" },
      ],
      "id", 
      { chunksSize: 2 }
    );

Stream

const previousStream = new ReadableStream({
      start(controller) {
        previousArray.forEach((value) => controller.enqueue(value));
        controller.close();
      },
    }); 
    
const nextStream = new ReadableStream({
      start(controller) {
        nextArray.forEach((value) => controller.enqueue(value));
        controller.close();
      },
    }); 
    
const diff = streamListDiff(previousStream, nextStream, "id", { chunksSize: 2 });

File

const previousFile = new File([JSON.stringify(previousArrayFile)], 
                              "previousArray.json", 
                              { type: "application/json" }); 
                              
const nextFile = new File([JSON.stringify(nextArrayFile)], 
                              "nextArray.json", 
                              { type: "application/json" }); 
                              
const diff = streamListDiff(previousFile, nextFile, "id", { chunksSize: 10 });
 

Example

const diff = streamListDiff(
      [ 
-       { id: 1, name: "Item 1" },  
        { id: 2, name: "Item 2" },
        { id: 3, name: "Item 3" } 
      ],
      [
+       { id: 0, name: "Item 0" }, 
        { id: 2, name: "Item 2" },
+       { id: 3, name: "Item Three" },
      ],
      "id", 
      { chunksSize: 2 }
    );

Output

diff.on("data", (chunk) => {
      // first chunk received (2 object diffs)
      [
+       {
+         previousValue: null,
+         currentValue: { id: 0, name: 'Item 0' },
+         prevIndex: null,
+         newIndex: 0,
+         indexDiff: null,
+         status: 'added'
+       },
-       {
-         previousValue: { id: 1, name: 'Item 1' },
-         currentValue: null,
-         prevIndex: 0,
-         newIndex: null,
-         indexDiff: null,
-         status: 'deleted'
-       }
      ]
    // second chunk received (2 object diffs)
      [
        {
          previousValue: { id: 2, name: 'Item 2' },
          currentValue: { id: 2, name: 'Item 2' },
          prevIndex: 1,
          newIndex: 1,
          indexDiff: 0,
          status: 'equal'
        },
+       {
+         previousValue: { id: 3, name: 'Item 3' },
+         currentValue: { id: 3, name: 'Item Three' },
+         prevIndex: 2,
+         newIndex: 2,
+         indexDiff: 0,
+         status: 'updated'
+       },
     ]
});

diff.on("finish", () => console.log("The full diff is available."))
diff.on("error", (err) => console.log(err))

REACT

Here is an example of its use in a React application.

export function useListDiff() {
  const [list, setList] = useState<StreamListDiff<Data>[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);
  const [isError, setIsError] = useState<Error>();

  const getListDiff = useCallback(async () => {
    setIsStreaming(true);
    const [prevList, nextList] = await fetchLists()
    const diff = streamListDiff(prevList, nextList, "id", {
      chunksSize: 50000, // Large chunks size for fewer updates
      useWorker: true,
    });

    diff.on("data", (chunk) => {
      // Schedule state updates on the next frame for smooth rendering
      requestAnimationFrame(() => {
        setList((prev) => [...prev, ...chunk]);
      });
    });
    diff.on("finish", () => setIsStreaming(false));
    diff.on("error", (err) => {
      setIsStreaming(false);
      setIsError(err);
    });
  }, []);

  useEffect(() => {
    getListDiff();
  }, [getListDiff]);

  return {
    list,
    isStreaming,
    isError,
  };
}

Last updated