From 41f9b8ceeb185b928ab7c2b5d3c1dc21ca1e49ae Mon Sep 17 00:00:00 2001
From: streamich <streamich@gmail.com>
Date: Sun, 25 Jun 2023 14:54:58 +0200
Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20json=20encoding=20?=
 =?UTF-8?q?for=20snapshots?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/snapshot/__tests__/binary.test.ts |  9 ++++
 src/snapshot/__tests__/json.test.ts   | 71 +++++++++++++++++++++++++++
 src/snapshot/binary.ts                |  3 +-
 src/snapshot/index.ts                 |  1 +
 src/snapshot/json.ts                  | 32 ++++++++++++
 src/snapshot/shared.ts                |  3 ++
 6 files changed, 118 insertions(+), 1 deletion(-)
 create mode 100644 src/snapshot/__tests__/json.test.ts
 create mode 100644 src/snapshot/json.ts
 create mode 100644 src/snapshot/shared.ts

diff --git a/src/snapshot/__tests__/binary.test.ts b/src/snapshot/__tests__/binary.test.ts
index 384be1dbe..d7f762ff5 100644
--- a/src/snapshot/__tests__/binary.test.ts
+++ b/src/snapshot/__tests__/binary.test.ts
@@ -20,6 +20,15 @@ const data = {
   },
 };
 
+test('sync and async snapshots are equivalent', async () => {
+  const { fs } = memfs(data);
+  fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink');
+  fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3]));
+  const snapshot1 = binary.toBinarySnapshotSync({ fs: fs, path: '/start' })!;
+  const snapshot2 = await binary.toBinarySnapshot({ fs: fs.promises, path: '/start' })!;
+  expect(snapshot1).toStrictEqual(snapshot2);
+}); 
+
 describe('synchronous', () => {
   test('can create a binary snapshot and un-snapshot it back', () => {
     const { fs } = memfs(data);
diff --git a/src/snapshot/__tests__/json.test.ts b/src/snapshot/__tests__/json.test.ts
new file mode 100644
index 000000000..9aae663d5
--- /dev/null
+++ b/src/snapshot/__tests__/json.test.ts
@@ -0,0 +1,71 @@
+import { memfs } from '../..';
+import {SnapshotNodeType} from '../constants';
+import * as json from '../json';
+
+const data = {
+  '/start': {
+    file1: 'file1',
+    file2: 'file2',
+    'empty-folder': null,
+    '/folder1': {
+      file3: 'file3',
+      file4: 'file4',
+      'empty-folder': null,
+      '/folder2': {
+        file5: 'file5',
+        file6: 'file6',
+        'empty-folder': null,
+        'empty-folde2': null,
+      },
+    },
+  },
+};
+
+test('snapshot is a valid JSON', () => {
+  const { fs } = memfs(data);
+  fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink');
+  fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3]));
+  const snapshot = json.toJsonSnapshotSync({ fs, path: '/start' })!;
+  const pojo = JSON.parse(Buffer.from(snapshot).toString());
+  expect(Array.isArray(pojo)).toBe(true);
+  expect(pojo[0]).toBe(SnapshotNodeType.Folder);
+});
+
+test('sync and async snapshots are equivalent', async () => {
+  const { fs } = memfs(data);
+  fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink');
+  fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3]));
+  const snapshot1 = await json.toJsonSnapshotSync({ fs: fs, path: '/start' })!;
+  const snapshot2 = await json.toJsonSnapshot({ fs: fs.promises, path: '/start' })!;;
+  expect(snapshot1).toStrictEqual(snapshot2);
+});
+
+describe('synchronous', () => {
+  test('can create a binary snapshot and un-snapshot it back', () => {
+    const { fs } = memfs(data);
+    fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink');
+    fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3]));
+    const snapshot = json.toJsonSnapshotSync({ fs, path: '/start' })!;
+    const { fs: fs2, vol: vol2 } = memfs();
+    fs2.mkdirSync('/start', { recursive: true });
+    json.fromJsonSnapshotSync(snapshot, { fs: fs2, path: '/start' });
+    expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3]));
+    const snapshot2 = json.toJsonSnapshotSync({ fs: fs2, path: '/start' })!;
+    expect(snapshot2).toStrictEqual(snapshot);
+  });
+});
+
+describe('asynchronous', () => {
+  test('can create a binary snapshot and un-snapshot it back', async () => {
+    const { fs } = memfs(data);
+    fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink');
+    fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3]));
+    const snapshot = await json.toJsonSnapshot({ fs: fs.promises, path: '/start' })!;
+    const { fs: fs2, vol: vol2 } = memfs();
+    fs2.mkdirSync('/start', { recursive: true });
+    await json.fromJsonSnapshot(snapshot, { fs: fs2.promises, path: '/start' });
+    expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3]));
+    const snapshot2 = await json.toJsonSnapshot({ fs: fs2.promises, path: '/start' })!;
+    expect(snapshot2).toStrictEqual(snapshot);
+  });
+});
diff --git a/src/snapshot/binary.ts b/src/snapshot/binary.ts
index 2ead2fa2e..f92ba5153 100644
--- a/src/snapshot/binary.ts
+++ b/src/snapshot/binary.ts
@@ -2,10 +2,11 @@ import {CborEncoder} from 'json-joy/es6/json-pack/cbor/CborEncoder';
 import {CborDecoder} from 'json-joy/es6/json-pack/cbor/CborDecoder';
 import {fromSnapshotSync, toSnapshotSync} from './sync';
 import {fromSnapshot, toSnapshot} from './async';
+import {writer} from './shared';
 import type {CborUint8Array} from 'json-joy/es6/json-pack/cbor/types';
 import type {AsyncSnapshotOptions, SnapshotNode, SnapshotOptions} from './types';
 
-const encoder = new CborEncoder();
+const encoder = new CborEncoder(writer);
 const decoder = new CborDecoder();
 
 export const toBinarySnapshotSync = (options: SnapshotOptions): CborUint8Array<SnapshotNode> => {
diff --git a/src/snapshot/index.ts b/src/snapshot/index.ts
index 66b91fb83..5c05d1fda 100644
--- a/src/snapshot/index.ts
+++ b/src/snapshot/index.ts
@@ -2,3 +2,4 @@ export type  * from './types';
 export * from './constants';
 export * from './sync';
 export * from './binary';
+export * from './json';
diff --git a/src/snapshot/json.ts b/src/snapshot/json.ts
new file mode 100644
index 000000000..a180ee864
--- /dev/null
+++ b/src/snapshot/json.ts
@@ -0,0 +1,32 @@
+import {JsonEncoder} from 'json-joy/es6/json-pack/json/JsonEncoder';
+import {JsonDecoder} from 'json-joy/es6/json-pack/json/JsonDecoder';
+import {fromSnapshotSync, toSnapshotSync} from './sync';
+import {fromSnapshot, toSnapshot} from './async';
+import {writer} from './shared';
+import type {AsyncSnapshotOptions, SnapshotNode, SnapshotOptions} from './types';
+
+/** @todo Import this type from `json-joy` once it is available. */
+export type JsonUint8Array<T> = Uint8Array & {__BRAND__: 'json'; __TYPE__: T};
+
+const encoder = new JsonEncoder(writer);
+const decoder = new JsonDecoder();
+
+export const toJsonSnapshotSync = (options: SnapshotOptions): JsonUint8Array<SnapshotNode> => {
+  const snapshot = toSnapshotSync(options);
+  return encoder.encode(snapshot) as JsonUint8Array<SnapshotNode>;
+};
+
+export const fromJsonSnapshotSync = (uint8: JsonUint8Array<SnapshotNode>, options: SnapshotOptions): void => {
+  const snapshot = decoder.read(uint8) as SnapshotNode;
+  fromSnapshotSync(snapshot, options);
+};
+
+export const toJsonSnapshot = async (options: AsyncSnapshotOptions): Promise<JsonUint8Array<SnapshotNode>> => {
+  const snapshot = await toSnapshot(options);
+  return encoder.encode(snapshot) as JsonUint8Array<SnapshotNode>;
+};
+
+export const fromJsonSnapshot = async (uint8: JsonUint8Array<SnapshotNode>, options: AsyncSnapshotOptions): Promise<void> => {
+  const snapshot = decoder.read(uint8) as SnapshotNode;
+  await fromSnapshot(snapshot, options);
+};
diff --git a/src/snapshot/shared.ts b/src/snapshot/shared.ts
new file mode 100644
index 000000000..f592b2417
--- /dev/null
+++ b/src/snapshot/shared.ts
@@ -0,0 +1,3 @@
+import {Writer} from 'json-joy/es6/util/buffers/Writer';
+
+export const writer = new Writer(1024 * 32);