@fieldnotes/sync-redis 0.3.1 → 0.4.0
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.
- package/README.md +4 -0
- package/dist/index.cjs +40 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +44 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -81,6 +81,10 @@ publishes its live ops to a single channel and every other instance forwards the
|
|
|
81
81
|
Like the backend, it has **no Redis dependency of its own** — you inject two connections via the
|
|
82
82
|
`RedisPublisher` / `RedisSubscriber` seams.
|
|
83
83
|
|
|
84
|
+
`publish()` resolves only after Redis acknowledges the command. If Redis rejects it, the fanout
|
|
85
|
+
invokes the optional `onError` observer and rethrows so `SyncHub` can surface the failed durable
|
|
86
|
+
mutation publication. Do not discard the returned promise when publishing directly.
|
|
87
|
+
|
|
84
88
|
> **The subscriber MUST be a dedicated connection.** Redis forbids any other command on a connection that is
|
|
85
89
|
> in subscribe mode, so publish and subscribe cannot share one connection — that is why `RedisHubFanout`
|
|
86
90
|
> takes two.
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,9 @@ var RedisHubBackend = class {
|
|
|
37
37
|
key(room) {
|
|
38
38
|
return `${this.keyPrefix}${room}`;
|
|
39
39
|
}
|
|
40
|
+
layersKey(room) {
|
|
41
|
+
return `${this.keyPrefix}${room}:layers`;
|
|
42
|
+
}
|
|
40
43
|
async snapshot(room) {
|
|
41
44
|
const map = await this.client.hGetAll(this.key(room));
|
|
42
45
|
const out = [];
|
|
@@ -68,6 +71,33 @@ var RedisHubBackend = class {
|
|
|
68
71
|
else if (op.kind === "remove") await this.client.hDel(key, op.id);
|
|
69
72
|
else if (op.kind === "clear") await this.client.del(key);
|
|
70
73
|
}
|
|
74
|
+
async layerRecords(room) {
|
|
75
|
+
const map = await this.client.hGetAll(this.layersKey(room));
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const value of Object.values(map)) {
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(value);
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if ((0, import_sync.isValidLayerRecord)(parsed)) out.push(parsed);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
async getLayerRecord(room, id) {
|
|
89
|
+
const value = await this.client.hGet(this.layersKey(room), id);
|
|
90
|
+
if (value == null) return void 0;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(value);
|
|
93
|
+
return (0, import_sync.isValidLayerRecord)(parsed) ? parsed : void 0;
|
|
94
|
+
} catch {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async applyLayerRecord(room, record) {
|
|
99
|
+
await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));
|
|
100
|
+
}
|
|
71
101
|
};
|
|
72
102
|
|
|
73
103
|
// src/redis-hub-fanout.ts
|
|
@@ -84,8 +114,16 @@ var RedisHubFanout = class {
|
|
|
84
114
|
this.channel = options.channel ?? "fieldnotes:fanout";
|
|
85
115
|
this.onError = options.onError ?? (() => void 0);
|
|
86
116
|
}
|
|
87
|
-
publish(payload) {
|
|
88
|
-
|
|
117
|
+
async publish(payload) {
|
|
118
|
+
try {
|
|
119
|
+
await this.publisher.publish(this.channel, payload);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
try {
|
|
122
|
+
this.onError(error);
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
89
127
|
}
|
|
90
128
|
subscribe(handler) {
|
|
91
129
|
this.handlers.add(handler);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["export { RedisHubBackend } from './redis-hub-backend';\
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["export { RedisHubBackend } from './redis-hub-backend';\nexport type { RedisHubBackendOptions } from './redis-hub-backend';\nexport type { RedisHashClient } from './redis-hash-client';\nexport { RedisHubFanout } from './redis-hub-fanout';\nexport type { RedisHubFanoutOptions } from './redis-hub-fanout';\nexport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\n","import {\n isValidElement,\n isValidLayerRecord,\n type LayerRecord,\n type SyncOp,\n} from '@fieldnotes/sync';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string; // default 'fieldnotes:room:'\n}\n\nexport class RedisHubBackend implements HubBackend {\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${room}`;\n }\n\n private layersKey(room: string): string {\n return `${this.keyPrefix}${room}:layers`;\n }\n\n async snapshot(room: string): Promise<CanvasElement[]> {\n const map = await this.client.hGetAll(this.key(room));\n const out: CanvasElement[] = [];\n for (const value of Object.values(map)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole snapshot\n }\n if (isValidElement(parsed)) out.push(parsed);\n }\n return out;\n }\n\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\n const value = await this.client.hGet(this.key(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidElement(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async apply(room: string, op: SyncOp): Promise<void> {\n const key = this.key(room);\n if (op.kind === 'upsert')\n await this.client.hSet(key, op.element.id, JSON.stringify(op.element));\n else if (op.kind === 'remove') await this.client.hDel(key, op.id);\n // 'clear' deletes elements only; the layer ledger is a separate hash and survives.\n else if (op.kind === 'clear') await this.client.del(key);\n // request-snapshot/snapshot never reach apply (the hub only applies data ops)\n }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n const map = await this.client.hGetAll(this.layersKey(room));\n const out: LayerRecord[] = [];\n for (const value of Object.values(map)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole ledger\n }\n if (isValidLayerRecord(parsed)) out.push(parsed);\n }\n return out;\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const value = await this.client.hGet(this.layersKey(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidLayerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));\n }\n}\n","import type { HubFanout } from '@fieldnotes/sync-server';\nimport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\n\nexport interface RedisHubFanoutOptions {\n channel?: string;\n onError?: (err: unknown) => void;\n}\n\nexport class RedisHubFanout implements HubFanout {\n private readonly publisher: RedisPublisher;\n private readonly subscriber: RedisSubscriber;\n private readonly channel: string;\n private readonly onError: (err: unknown) => void;\n private readonly handlers = new Set<(payload: string) => void>();\n private subscribed = false;\n\n constructor(\n publisher: RedisPublisher,\n subscriber: RedisSubscriber,\n options: RedisHubFanoutOptions = {},\n ) {\n this.publisher = publisher;\n this.subscriber = subscriber;\n this.channel = options.channel ?? 'fieldnotes:fanout';\n this.onError = options.onError ?? (() => undefined);\n }\n\n async publish(payload: string): Promise<void> {\n try {\n await this.publisher.publish(this.channel, payload);\n } catch (error) {\n try {\n this.onError(error);\n } catch {\n /* preserve the publication failure even when the observer throws */\n }\n throw error;\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n if (!this.subscribed) {\n this.subscribed = true;\n Promise.resolve(\n this.subscriber.subscribe(this.channel, (message) => {\n for (const h of this.handlers) {\n try {\n h(message);\n } catch {\n /* isolate handlers */\n }\n }\n }),\n ).catch(this.onError);\n }\n return () => this.handlers.delete(handler);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SyncOp } from '@fieldnotes/sync';
|
|
1
|
+
import { SyncOp, LayerRecord } from '@fieldnotes/sync';
|
|
2
2
|
import { CanvasElement } from '@fieldnotes/core';
|
|
3
3
|
import { HubBackend, HubFanout } from '@fieldnotes/sync-server';
|
|
4
4
|
|
|
@@ -18,9 +18,13 @@ declare class RedisHubBackend implements HubBackend {
|
|
|
18
18
|
private readonly keyPrefix;
|
|
19
19
|
constructor(client: RedisHashClient, options?: RedisHubBackendOptions);
|
|
20
20
|
private key;
|
|
21
|
+
private layersKey;
|
|
21
22
|
snapshot(room: string): Promise<CanvasElement[]>;
|
|
22
23
|
get(room: string, id: string): Promise<CanvasElement | undefined>;
|
|
23
24
|
apply(room: string, op: SyncOp): Promise<void>;
|
|
25
|
+
layerRecords(room: string): Promise<LayerRecord[]>;
|
|
26
|
+
getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
27
|
+
applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
interface RedisPublisher {
|
|
@@ -42,7 +46,7 @@ declare class RedisHubFanout implements HubFanout {
|
|
|
42
46
|
private readonly handlers;
|
|
43
47
|
private subscribed;
|
|
44
48
|
constructor(publisher: RedisPublisher, subscriber: RedisSubscriber, options?: RedisHubFanoutOptions);
|
|
45
|
-
publish(payload: string): void
|
|
49
|
+
publish(payload: string): Promise<void>;
|
|
46
50
|
subscribe(handler: (payload: string) => void): () => void;
|
|
47
51
|
}
|
|
48
52
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SyncOp } from '@fieldnotes/sync';
|
|
1
|
+
import { SyncOp, LayerRecord } from '@fieldnotes/sync';
|
|
2
2
|
import { CanvasElement } from '@fieldnotes/core';
|
|
3
3
|
import { HubBackend, HubFanout } from '@fieldnotes/sync-server';
|
|
4
4
|
|
|
@@ -18,9 +18,13 @@ declare class RedisHubBackend implements HubBackend {
|
|
|
18
18
|
private readonly keyPrefix;
|
|
19
19
|
constructor(client: RedisHashClient, options?: RedisHubBackendOptions);
|
|
20
20
|
private key;
|
|
21
|
+
private layersKey;
|
|
21
22
|
snapshot(room: string): Promise<CanvasElement[]>;
|
|
22
23
|
get(room: string, id: string): Promise<CanvasElement | undefined>;
|
|
23
24
|
apply(room: string, op: SyncOp): Promise<void>;
|
|
25
|
+
layerRecords(room: string): Promise<LayerRecord[]>;
|
|
26
|
+
getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
27
|
+
applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
interface RedisPublisher {
|
|
@@ -42,7 +46,7 @@ declare class RedisHubFanout implements HubFanout {
|
|
|
42
46
|
private readonly handlers;
|
|
43
47
|
private subscribed;
|
|
44
48
|
constructor(publisher: RedisPublisher, subscriber: RedisSubscriber, options?: RedisHubFanoutOptions);
|
|
45
|
-
publish(payload: string): void
|
|
49
|
+
publish(payload: string): Promise<void>;
|
|
46
50
|
subscribe(handler: (payload: string) => void): () => void;
|
|
47
51
|
}
|
|
48
52
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// src/redis-hub-backend.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isValidElement,
|
|
4
|
+
isValidLayerRecord
|
|
5
|
+
} from "@fieldnotes/sync";
|
|
3
6
|
var RedisHubBackend = class {
|
|
4
7
|
client;
|
|
5
8
|
keyPrefix;
|
|
@@ -10,6 +13,9 @@ var RedisHubBackend = class {
|
|
|
10
13
|
key(room) {
|
|
11
14
|
return `${this.keyPrefix}${room}`;
|
|
12
15
|
}
|
|
16
|
+
layersKey(room) {
|
|
17
|
+
return `${this.keyPrefix}${room}:layers`;
|
|
18
|
+
}
|
|
13
19
|
async snapshot(room) {
|
|
14
20
|
const map = await this.client.hGetAll(this.key(room));
|
|
15
21
|
const out = [];
|
|
@@ -41,6 +47,33 @@ var RedisHubBackend = class {
|
|
|
41
47
|
else if (op.kind === "remove") await this.client.hDel(key, op.id);
|
|
42
48
|
else if (op.kind === "clear") await this.client.del(key);
|
|
43
49
|
}
|
|
50
|
+
async layerRecords(room) {
|
|
51
|
+
const map = await this.client.hGetAll(this.layersKey(room));
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const value of Object.values(map)) {
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(value);
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isValidLayerRecord(parsed)) out.push(parsed);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
async getLayerRecord(room, id) {
|
|
65
|
+
const value = await this.client.hGet(this.layersKey(room), id);
|
|
66
|
+
if (value == null) return void 0;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(value);
|
|
69
|
+
return isValidLayerRecord(parsed) ? parsed : void 0;
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async applyLayerRecord(room, record) {
|
|
75
|
+
await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));
|
|
76
|
+
}
|
|
44
77
|
};
|
|
45
78
|
|
|
46
79
|
// src/redis-hub-fanout.ts
|
|
@@ -57,8 +90,16 @@ var RedisHubFanout = class {
|
|
|
57
90
|
this.channel = options.channel ?? "fieldnotes:fanout";
|
|
58
91
|
this.onError = options.onError ?? (() => void 0);
|
|
59
92
|
}
|
|
60
|
-
publish(payload) {
|
|
61
|
-
|
|
93
|
+
async publish(payload) {
|
|
94
|
+
try {
|
|
95
|
+
await this.publisher.publish(this.channel, payload);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
try {
|
|
98
|
+
this.onError(error);
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
62
103
|
}
|
|
63
104
|
subscribe(handler) {
|
|
64
105
|
this.handlers.add(handler);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../src/redis-hub-backend.ts","../src/redis-hub-fanout.ts"],"sourcesContent":["import {\n isValidElement,\n isValidLayerRecord,\n type LayerRecord,\n type SyncOp,\n} from '@fieldnotes/sync';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { HubBackend } from '@fieldnotes/sync-server';\nimport type { RedisHashClient } from './redis-hash-client';\n\nexport interface RedisHubBackendOptions {\n keyPrefix?: string; // default 'fieldnotes:room:'\n}\n\nexport class RedisHubBackend implements HubBackend {\n private readonly client: RedisHashClient;\n private readonly keyPrefix: string;\n\n constructor(client: RedisHashClient, options: RedisHubBackendOptions = {}) {\n this.client = client;\n this.keyPrefix = options.keyPrefix ?? 'fieldnotes:room:';\n }\n\n private key(room: string): string {\n return `${this.keyPrefix}${room}`;\n }\n\n private layersKey(room: string): string {\n return `${this.keyPrefix}${room}:layers`;\n }\n\n async snapshot(room: string): Promise<CanvasElement[]> {\n const map = await this.client.hGetAll(this.key(room));\n const out: CanvasElement[] = [];\n for (const value of Object.values(map)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole snapshot\n }\n if (isValidElement(parsed)) out.push(parsed);\n }\n return out;\n }\n\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\n const value = await this.client.hGet(this.key(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidElement(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async apply(room: string, op: SyncOp): Promise<void> {\n const key = this.key(room);\n if (op.kind === 'upsert')\n await this.client.hSet(key, op.element.id, JSON.stringify(op.element));\n else if (op.kind === 'remove') await this.client.hDel(key, op.id);\n // 'clear' deletes elements only; the layer ledger is a separate hash and survives.\n else if (op.kind === 'clear') await this.client.del(key);\n // request-snapshot/snapshot never reach apply (the hub only applies data ops)\n }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n const map = await this.client.hGetAll(this.layersKey(room));\n const out: LayerRecord[] = [];\n for (const value of Object.values(map)) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n continue; // skip a corrupt stored value rather than throwing the whole ledger\n }\n if (isValidLayerRecord(parsed)) out.push(parsed);\n }\n return out;\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const value = await this.client.hGet(this.layersKey(room), id);\n if (value == null) return undefined;\n try {\n const parsed: unknown = JSON.parse(value);\n return isValidLayerRecord(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n await this.client.hSet(this.layersKey(room), record.id, JSON.stringify(record));\n }\n}\n","import type { HubFanout } from '@fieldnotes/sync-server';\nimport type { RedisPublisher, RedisSubscriber } from './redis-fanout-client';\n\nexport interface RedisHubFanoutOptions {\n channel?: string;\n onError?: (err: unknown) => void;\n}\n\nexport class RedisHubFanout implements HubFanout {\n private readonly publisher: RedisPublisher;\n private readonly subscriber: RedisSubscriber;\n private readonly channel: string;\n private readonly onError: (err: unknown) => void;\n private readonly handlers = new Set<(payload: string) => void>();\n private subscribed = false;\n\n constructor(\n publisher: RedisPublisher,\n subscriber: RedisSubscriber,\n options: RedisHubFanoutOptions = {},\n ) {\n this.publisher = publisher;\n this.subscriber = subscriber;\n this.channel = options.channel ?? 'fieldnotes:fanout';\n this.onError = options.onError ?? (() => undefined);\n }\n\n async publish(payload: string): Promise<void> {\n try {\n await this.publisher.publish(this.channel, payload);\n } catch (error) {\n try {\n this.onError(error);\n } catch {\n /* preserve the publication failure even when the observer throws */\n }\n throw error;\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n if (!this.subscribed) {\n this.subscribed = true;\n Promise.resolve(\n this.subscriber.subscribe(this.channel, (message) => {\n for (const h of this.handlers) {\n try {\n h(message);\n } catch {\n /* isolate handlers */\n }\n }\n }),\n ).catch(this.onError);\n }\n return () => this.handlers.delete(handler);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldnotes/sync-redis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Redis-backed HubBackend for Field Notes real-time sync relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"fieldnotes"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fieldnotes/sync": "0.
|
|
39
|
+
"@fieldnotes/sync": "0.10.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@vitest/coverage-v8": "^4.1.0",
|
|
43
43
|
"tsup": "^8.5.1",
|
|
44
44
|
"vitest": "^4.1.0",
|
|
45
|
-
"@fieldnotes/
|
|
46
|
-
"@fieldnotes/
|
|
45
|
+
"@fieldnotes/sync-server": "0.12.0",
|
|
46
|
+
"@fieldnotes/core": "0.53.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsup",
|