@grayfalcon666/loom 1.0.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 +161 -0
- package/embedded_wasm.js +5 -0
- package/index.js +137 -0
- package/loom.d.ts +139 -0
- package/loom.js +94 -0
- package/loom.wasm +0 -0
- package/package.json +48 -0
- package/wasm_exec.js +575 -0
package/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grayfalcon/loom — entry point
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* init(wasmUrl?) — async, returns LoomAPI (WASM is embedded as base64 by default)
|
|
6
|
+
* default export — same as named { init }
|
|
7
|
+
*
|
|
8
|
+
* Zero-config usage (WASM embedded, no extra files):
|
|
9
|
+
* import { init } from '@grayfalcon/loom';
|
|
10
|
+
* const loom = await init();
|
|
11
|
+
*
|
|
12
|
+
* Custom WASM URL (e.g. from /slim export):
|
|
13
|
+
* import { init } from '@grayfalcon/loom/slim';
|
|
14
|
+
* const loom = await init('/node_modules/@grayfalcon/loom/loom.wasm');
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import wasmExecJs from './wasm_exec.js';
|
|
18
|
+
import { loomWasmB64 } from './embedded_wasm.js';
|
|
19
|
+
|
|
20
|
+
/** @type {typeof import('./wasm_exec.js')} */
|
|
21
|
+
const Go = wasmExecJs.Go;
|
|
22
|
+
|
|
23
|
+
let _rawLoom = null;
|
|
24
|
+
let _initPromise = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} [wasmUrl] Optional. If omitted, uses the embedded base64 WASM.
|
|
28
|
+
* Pass a URL to a loom.wasm file (useful for /slim export).
|
|
29
|
+
* @returns {Promise<import('./loom').LoomAPI>}
|
|
30
|
+
*/
|
|
31
|
+
export async function init(wasmUrl) {
|
|
32
|
+
if (_rawLoom) {
|
|
33
|
+
return buildAPI();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (_initPromise) {
|
|
37
|
+
await _initPromise;
|
|
38
|
+
return buildAPI();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_initPromise = (async () => {
|
|
42
|
+
if (wasmUrl) {
|
|
43
|
+
// URL-based loading: fetch the .wasm file from the network
|
|
44
|
+
await loadWasmFromUrl(wasmUrl);
|
|
45
|
+
} else {
|
|
46
|
+
// Embedded loading: decode base64 WASM, no network request needed
|
|
47
|
+
await loadWasmFromBase64();
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
await _initPromise;
|
|
52
|
+
return buildAPI();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadWasmFromUrl(url) {
|
|
56
|
+
const go = new Go();
|
|
57
|
+
const response = await fetch(url);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`[loom] Failed to fetch WASM from ${url}: HTTP ${response.status}`);
|
|
60
|
+
}
|
|
61
|
+
const result = await WebAssembly.instantiateStreaming(response, go.importObject);
|
|
62
|
+
go.run(result.instance);
|
|
63
|
+
_rawLoom = await waitForLoom();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadWasmFromBase64() {
|
|
67
|
+
// Decode base64 to Uint8Array — no fetch, no CORS, no extra files
|
|
68
|
+
const binaryString = atob(loomWasmB64);
|
|
69
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
70
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
71
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const go = new Go();
|
|
75
|
+
const result = await WebAssembly.instantiate(bytes, go.importObject);
|
|
76
|
+
go.run(result.instance);
|
|
77
|
+
_rawLoom = await waitForLoom();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function waitForLoom() {
|
|
81
|
+
let attempts = 0;
|
|
82
|
+
while (!_rawLoom && attempts < 100) {
|
|
83
|
+
await new Promise(r => setTimeout(r, 20));
|
|
84
|
+
_rawLoom = globalThis.loom ?? null;
|
|
85
|
+
attempts++;
|
|
86
|
+
}
|
|
87
|
+
if (!_rawLoom) {
|
|
88
|
+
throw new Error('[loom] Go init() did not register the loom namespace within 2s');
|
|
89
|
+
}
|
|
90
|
+
return _rawLoom;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildAPI() {
|
|
94
|
+
const api = {
|
|
95
|
+
// Core API — camelCase (preferred)
|
|
96
|
+
newDocument: (actorID) => _rawLoom.newDocument(actorID),
|
|
97
|
+
freeDocument: (docID) => _rawLoom.freeDocument(docID),
|
|
98
|
+
set: (docID, k, v) => _rawLoom.set(docID, k, v),
|
|
99
|
+
get: (docID, k) => _rawLoom.get(docID, k),
|
|
100
|
+
addText: (docID, k, tid) => _rawLoom.addText(docID, k, tid),
|
|
101
|
+
insertChar: (docID, tid, idx, code) => _rawLoom.insertChar(docID, tid, idx, code),
|
|
102
|
+
deleteChar: (docID, tid, idx) => _rawLoom.deleteChar(docID, tid, idx),
|
|
103
|
+
peekText: (docID, tid) => _rawLoom.peekText(docID, tid),
|
|
104
|
+
textString: (docID, tid) => _rawLoom.textString(docID, tid),
|
|
105
|
+
getText: (docID, tid) => _rawLoom.getText(docID, tid),
|
|
106
|
+
receive: (docID, opJSON) => _rawLoom.receive(docID, opJSON),
|
|
107
|
+
unwrapReceive: (docID, opJSON) => _rawLoom.unwrapReceive(docID, opJSON),
|
|
108
|
+
save: (docID) => _rawLoom.save(docID),
|
|
109
|
+
load: (snapshotJSON) => _rawLoom.load(snapshotJSON),
|
|
110
|
+
getActorID: (docID) => _rawLoom.getActorID(docID),
|
|
111
|
+
|
|
112
|
+
// Legacy / demo.html compatibility aliases
|
|
113
|
+
Loom_NewDocument: (aid) => _rawLoom.newDocument(aid),
|
|
114
|
+
Loom_FreeDocument: (docID) => _rawLoom.freeDocument(docID),
|
|
115
|
+
Loom_Set: (docID, k, v) => _rawLoom.set(docID, k, v),
|
|
116
|
+
Loom_Get: (docID, k) => _rawLoom.get(docID, k),
|
|
117
|
+
Loom_AddText: (docID, k, tid) => _rawLoom.addText(docID, k, tid),
|
|
118
|
+
Loom_InsertChar: (docID, tid, idx, c) => _rawLoom.insertChar(docID, tid, idx, c),
|
|
119
|
+
Loom_DeleteChar: (docID, tid, idx) => _rawLoom.deleteChar(docID, tid, idx),
|
|
120
|
+
Loom_TextString: (docID, tid) => _rawLoom.textString(docID, tid),
|
|
121
|
+
Loom_PeekText: (docID, tid) => _rawLoom.peekText(docID, tid),
|
|
122
|
+
Loom_Receive: (docID, op) => _rawLoom.receive(docID, op),
|
|
123
|
+
Loom_UnwrapReceive: (docID, op) => _rawLoom.unwrapReceive(docID, op),
|
|
124
|
+
Loom_Save: (docID) => _rawLoom.save(docID),
|
|
125
|
+
Loom_Load: (snap) => _rawLoom.load(snap),
|
|
126
|
+
Loom_GetActorID: (docID) => _rawLoom.getActorID(docID),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Also expose on globalThis for debug / non-module environments
|
|
130
|
+
globalThis.loom = api;
|
|
131
|
+
return api;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Also patch globalThis.Loom for the legacy demo.html pattern
|
|
135
|
+
globalThis.Loom = { init };
|
|
136
|
+
|
|
137
|
+
export default { init };
|
package/loom.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loom CRDT — TypeScript declarations
|
|
3
|
+
*
|
|
4
|
+
* The Loom CRDT engine runs in the browser as a WebAssembly module,
|
|
5
|
+
* powered by Go's syscall/js. A single call to Loom.init() loads
|
|
6
|
+
* the WASM binary and registers all functions.
|
|
7
|
+
*
|
|
8
|
+
* Basic usage:
|
|
9
|
+
* import { init } from '@grayfalcon/loom';
|
|
10
|
+
* const loom = await init();
|
|
11
|
+
* const doc = loom.newDocument('alice');
|
|
12
|
+
* loom.addText(doc, 'body', 'text-1');
|
|
13
|
+
* const op = loom.insertChar(doc, 'text-1', 0, 'H'.charCodeAt(0));
|
|
14
|
+
* ws.send(op); // send to your WS server
|
|
15
|
+
* loom.unwrapReceive(otherDoc, op); // apply remote op
|
|
16
|
+
* console.log(loom.peekText(doc, 'text-1')); // 'H'
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface LoomAPI {
|
|
20
|
+
/**
|
|
21
|
+
* Create a new CRDT document for the given actor.
|
|
22
|
+
* Returns a numeric document ID used in all subsequent calls.
|
|
23
|
+
*/
|
|
24
|
+
newDocument(actorID: string): number;
|
|
25
|
+
|
|
26
|
+
/** Free a document from WASM memory. Call when done. */
|
|
27
|
+
freeDocument(docID: number): void;
|
|
28
|
+
|
|
29
|
+
// ── LWWMap (key-value) ────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Set a key-value pair in the document's root map. Returns Op JSON string. */
|
|
32
|
+
set(docID: number, key: string, value: string): string | null;
|
|
33
|
+
|
|
34
|
+
/** Get the current value for a key in the root map. Returns JSON string or null. */
|
|
35
|
+
get(docID: number, key: string): string | null;
|
|
36
|
+
|
|
37
|
+
// ── RGA String (collaborative text) ───────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Declare a shared text object at a key under the root map.
|
|
41
|
+
* Call this on both local and remote documents before inserting characters.
|
|
42
|
+
* Returns Op JSON string (send this to your WS server so peers can call addText too).
|
|
43
|
+
*/
|
|
44
|
+
addText(docID: number, key: string, textObjID: string): string | null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Insert a character at a given index into the shared text object.
|
|
48
|
+
* Returns Op JSON string — send this to your WS server for distribution.
|
|
49
|
+
*
|
|
50
|
+
* @param docID Document ID from newDocument()
|
|
51
|
+
* @param textObjID Text object ID, must match what addText() used
|
|
52
|
+
* @param index Character index (0 = before first char, len = after last)
|
|
53
|
+
* @param charCode Unicode code point (e.g. 'A'.charCodeAt(0) === 65)
|
|
54
|
+
*/
|
|
55
|
+
insertChar(docID: number, textObjID: string, index: number, charCode: number): string | null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Delete the character at a given index from the shared text object.
|
|
59
|
+
* Returns Op JSON string — send this to your WS server for distribution.
|
|
60
|
+
*
|
|
61
|
+
* @param index 0-based index of the character to delete
|
|
62
|
+
*/
|
|
63
|
+
deleteChar(docID: number, textObjID: string, index: number): string | null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the current text content as a plain JavaScript string.
|
|
67
|
+
* Aliases: peekText (identical), textString (identical)
|
|
68
|
+
*/
|
|
69
|
+
peekText(docID: number, textObjID: string): string;
|
|
70
|
+
|
|
71
|
+
/** @deprecated Alias for peekText. Use peekText or getText instead. */
|
|
72
|
+
textString(docID: number, textObjID: string): string;
|
|
73
|
+
|
|
74
|
+
/** @deprecated Alias for peekText. Use peekText instead. */
|
|
75
|
+
getText(docID: number, textObjID: string): string;
|
|
76
|
+
|
|
77
|
+
// ── Remote Ops ─────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Apply a remote Op (received from a peer) to the local document.
|
|
81
|
+
* Handles all Op types: set, addText, insertChar, deleteChar.
|
|
82
|
+
* Alias: unwrapReceive (identical).
|
|
83
|
+
*/
|
|
84
|
+
receive(docID: number, opJSON: string): void;
|
|
85
|
+
|
|
86
|
+
/** @deprecated Alias for receive. Use receive instead. */
|
|
87
|
+
unwrapReceive(docID: number, opJSON: string): void;
|
|
88
|
+
|
|
89
|
+
// ── Persistence ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Serialize the document to a JSON snapshot string.
|
|
93
|
+
* Use this to save document state to localStorage / your backend.
|
|
94
|
+
*/
|
|
95
|
+
save(docID: number): string;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load a document from a JSON snapshot (previously obtained via save()).
|
|
99
|
+
* Returns a new document ID.
|
|
100
|
+
*/
|
|
101
|
+
load(snapshotJSON: string): number;
|
|
102
|
+
|
|
103
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** Get the actor ID string for a document. */
|
|
106
|
+
getActorID(docID: number): string;
|
|
107
|
+
|
|
108
|
+
// ── Raw / Legacy aliases (used by demo.html) ──────────────────────────────
|
|
109
|
+
Loom_NewDocument: (actorID: string) => number;
|
|
110
|
+
Loom_FreeDocument: (docID: number) => void;
|
|
111
|
+
Loom_Set: (docID: number, key: string, value: string) => string | null;
|
|
112
|
+
Loom_Get: (docID: number, key: string) => string | null;
|
|
113
|
+
Loom_AddText: (docID: number, key: string, textObjID: string) => string | null;
|
|
114
|
+
Loom_InsertChar: (docID: number, textObjID: string, index: number, charCode: number) => string | null;
|
|
115
|
+
Loom_DeleteChar: (docID: number, textObjID: string, index: number) => string | null;
|
|
116
|
+
Loom_TextString: (docID: number, textObjID: string) => string;
|
|
117
|
+
Loom_PeekText: (docID: number, textObjID: string) => string;
|
|
118
|
+
Loom_Receive: (docID: number, opJSON: string) => void;
|
|
119
|
+
Loom_UnwrapReceive: (docID: number, opJSON: string) => void;
|
|
120
|
+
Loom_Save: (docID: number) => string;
|
|
121
|
+
Loom_Load: (snapshotJSON: string) => number;
|
|
122
|
+
Loom_GetActorID: (docID: number) => string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Initialize the Loom WASM module.
|
|
127
|
+
*
|
|
128
|
+
* The WASM binary is embedded as a base64 data URL, so no extra file copy is needed —
|
|
129
|
+
* just `npm install` and call init(). No CORS issues.
|
|
130
|
+
*
|
|
131
|
+
* @param wasmUrl Optional URL to a loom.wasm file.
|
|
132
|
+
* If omitted, uses the bundled base64-encoded WASM (recommended).
|
|
133
|
+
* Pass a custom URL only if you need to host the .wasm file yourself
|
|
134
|
+
* (e.g. for the /slim export).
|
|
135
|
+
*/
|
|
136
|
+
export function init(wasmUrl?: string): Promise<LoomAPI>;
|
|
137
|
+
|
|
138
|
+
/** @deprecated Use the named export `init` instead. */
|
|
139
|
+
export default { init };
|
package/loom.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loom.js — JavaScript wrapper for the Loom CRDT WASM module.
|
|
3
|
+
*
|
|
4
|
+
* Uses syscall/js API: Go registers loom.* functions via js.Global() in init().
|
|
5
|
+
* No manual memory management needed.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const loom = await Loom.init();
|
|
9
|
+
* const docID = loom.newDocument("alice");
|
|
10
|
+
* loom.set(docID, "title", "Hello");
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let _rawLoom = null;
|
|
14
|
+
|
|
15
|
+
async function init(wasmPath = "./loom.wasm") {
|
|
16
|
+
if (typeof globalThis.WebAssembly === "undefined") {
|
|
17
|
+
throw new Error("WebAssembly is not supported");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const wasmExecPath = wasmPath.replace(/[^/\\]+$/, "wasm_exec.js");
|
|
21
|
+
await import(wasmExecPath);
|
|
22
|
+
|
|
23
|
+
const GoCtor = globalThis.Go;
|
|
24
|
+
if (typeof GoCtor !== "function") {
|
|
25
|
+
throw new Error("wasm_exec.js did not set globalThis.Go");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const go = new GoCtor();
|
|
29
|
+
let instance;
|
|
30
|
+
const response = await fetch(wasmPath);
|
|
31
|
+
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
32
|
+
const result = await WebAssembly.instantiateStreaming(response, go.importObject);
|
|
33
|
+
instance = result.instance;
|
|
34
|
+
|
|
35
|
+
go.run(instance);
|
|
36
|
+
|
|
37
|
+
// Wait for Go init() to register loom.* functions
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
while (!globalThis.loom && attempts < 20) {
|
|
40
|
+
await new Promise(r => setTimeout(r, 10));
|
|
41
|
+
attempts++;
|
|
42
|
+
}
|
|
43
|
+
if (!globalThis.loom) {
|
|
44
|
+
throw new Error("Go init() did not register loom namespace");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_rawLoom = globalThis.loom;
|
|
48
|
+
|
|
49
|
+
const api = {
|
|
50
|
+
newDocument(actorID) { return _rawLoom.newDocument(actorID); },
|
|
51
|
+
freeDocument(docID) { _rawLoom.freeDocument(docID); },
|
|
52
|
+
set(docID, key, value) { return _rawLoom.set(docID, key, value); },
|
|
53
|
+
get(docID, key) { return _rawLoom.get(docID, key); },
|
|
54
|
+
addText(docID, key, textID) { return _rawLoom.addText(docID, key, textID); },
|
|
55
|
+
insertChar(docID, textID, idx, code) { return _rawLoom.insertChar(docID, textID, idx, code); },
|
|
56
|
+
deleteChar(docID, textID, idx) { return _rawLoom.deleteChar(docID, textID, idx); },
|
|
57
|
+
textString(docID, textID) { return _rawLoom.textString(docID, textID); },
|
|
58
|
+
receive(docID, opJSON) { return _rawLoom.receive(docID, opJSON); },
|
|
59
|
+
save(docID) { return _rawLoom.save(docID); },
|
|
60
|
+
load(snapshotJSON) { return _rawLoom.load(snapshotJSON); },
|
|
61
|
+
getActorID(docID) { return _rawLoom.getActorID(docID); },
|
|
62
|
+
getText(docID, textID) { return _rawLoom.getText(docID, textID); },
|
|
63
|
+
|
|
64
|
+
// Aliases for demo.html
|
|
65
|
+
peekText(docID, textID) {
|
|
66
|
+
return _rawLoom.peekText ? _rawLoom.peekText(docID, textID)
|
|
67
|
+
: _rawLoom.textString(docID, textID);
|
|
68
|
+
},
|
|
69
|
+
unwrapReceive(docID, opJSON) { return _rawLoom.receive(docID, opJSON); },
|
|
70
|
+
|
|
71
|
+
Loom_Set: (docID, k, v) => _rawLoom.set(docID, k, v),
|
|
72
|
+
Loom_InsertChar: (docID, tid, idx, c) => _rawLoom.insertChar(docID, tid, idx, c),
|
|
73
|
+
Loom_DeleteChar: (docID, tid, idx) => _rawLoom.deleteChar(docID, tid, idx),
|
|
74
|
+
Loom_Get: (docID, k) => _rawLoom.get(docID, k),
|
|
75
|
+
Loom_AddText: (docID, k, tid) => _rawLoom.addText(docID, k, tid),
|
|
76
|
+
Loom_TextString: (docID, tid) => _rawLoom.textString(docID, tid),
|
|
77
|
+
Loom_PeekText: (docID, tid) => api.peekText(docID, tid),
|
|
78
|
+
Loom_Receive: (docID, op) => _rawLoom.receive(docID, op),
|
|
79
|
+
Loom_UnwrapReceive:(docID, op) => _rawLoom.receive(docID, op),
|
|
80
|
+
Loom_Save: (docID) => _rawLoom.save(docID),
|
|
81
|
+
Loom_Load: (snap) => _rawLoom.load(snap),
|
|
82
|
+
Loom_NewDocument: (aid) => _rawLoom.newDocument(aid),
|
|
83
|
+
Loom_FreeDocument: (docID) => _rawLoom.freeDocument(docID),
|
|
84
|
+
Loom_GetActorID: (docID) => _rawLoom.getActorID(docID),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
globalThis.loom = api;
|
|
88
|
+
console.log("[loom] Ready. Functions:", Object.keys(_rawLoom).join(", "));
|
|
89
|
+
return api;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
globalThis.Loom = { init };
|
|
93
|
+
export { init };
|
|
94
|
+
export default { init };
|
package/loom.wasm
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grayfalcon666/loom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Go/WASM CRDT engine for collaborative text editing — runs in the browser with no build step",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./loom.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"browser": "./index.js",
|
|
11
|
+
"node": "./index.js",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./loom.js": "./loom.js",
|
|
15
|
+
"./loom.wasm": "./loom.wasm",
|
|
16
|
+
"./slim": "./loom.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"loom.js",
|
|
21
|
+
"loom.wasm",
|
|
22
|
+
"loom.d.ts",
|
|
23
|
+
"wasm_exec.js",
|
|
24
|
+
"embedded_wasm.js"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node scripts/build.mjs",
|
|
28
|
+
"prepublishOnly": "node scripts/build.mjs"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"crdt",
|
|
32
|
+
"collaborative",
|
|
33
|
+
"real-time",
|
|
34
|
+
"text-editing",
|
|
35
|
+
"wasm",
|
|
36
|
+
"automerge-alternative"
|
|
37
|
+
],
|
|
38
|
+
"author": "grayfalcon",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/grayfalcon666/loom"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"esbuild": "^0.24.0"
|
|
47
|
+
}
|
|
48
|
+
}
|