@levalicious/server-memory 0.0.11 → 0.0.13
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/binding.gyp +16 -0
- package/dist/index.js +4 -0
- package/dist/scripts/migrate-jsonl.js +169 -0
- package/dist/scripts/verify-migration.js +39 -0
- package/dist/server.js +711 -404
- package/dist/src/graphfile.js +560 -0
- package/dist/src/memoryfile.js +134 -0
- package/dist/src/pagerank.js +78 -0
- package/dist/src/stringtable.js +373 -0
- package/dist/tests/concurrency.test.js +189 -0
- package/dist/tests/memory-server.test.js +135 -24
- package/native/binding.c +340 -0
- package/native/memoryfile.c +343 -0
- package/native/memoryfile.h +82 -0
- package/package.json +8 -4
|
@@ -44,9 +44,10 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
44
44
|
await callTool(client, 'create_entities', {
|
|
45
45
|
entities: [{ name: 'Alice', entityType: 'Person', observations: ['First'] }]
|
|
46
46
|
});
|
|
47
|
+
// Exact same entity should be silently skipped
|
|
47
48
|
const result = await callTool(client, 'create_entities', {
|
|
48
49
|
entities: [
|
|
49
|
-
{ name: 'Alice', entityType: 'Person', observations: ['
|
|
50
|
+
{ name: 'Alice', entityType: 'Person', observations: ['First'] },
|
|
50
51
|
{ name: 'Bob', entityType: 'Person', observations: ['New'] }
|
|
51
52
|
]
|
|
52
53
|
});
|
|
@@ -54,6 +55,19 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
54
55
|
expect(result).toHaveLength(1);
|
|
55
56
|
expect(result[0].name).toBe('Bob');
|
|
56
57
|
});
|
|
58
|
+
it('should error on duplicate name with different data', async () => {
|
|
59
|
+
await callTool(client, 'create_entities', {
|
|
60
|
+
entities: [{ name: 'Alice', entityType: 'Person', observations: ['First'] }]
|
|
61
|
+
});
|
|
62
|
+
// Same name, different type — should error
|
|
63
|
+
await expect(callTool(client, 'create_entities', {
|
|
64
|
+
entities: [{ name: 'Alice', entityType: 'Organization', observations: ['First'] }]
|
|
65
|
+
})).rejects.toThrow(/already exists/);
|
|
66
|
+
// Same name, different observations — should error
|
|
67
|
+
await expect(callTool(client, 'create_entities', {
|
|
68
|
+
entities: [{ name: 'Alice', entityType: 'Person', observations: ['Different'] }]
|
|
69
|
+
})).rejects.toThrow(/already exists/);
|
|
70
|
+
});
|
|
57
71
|
it('should reject entities with more than 2 observations', async () => {
|
|
58
72
|
await expect(callTool(client, 'create_entities', {
|
|
59
73
|
entities: [{
|
|
@@ -257,13 +271,6 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
257
271
|
// A->B and A->C both have from='A' which is in the set
|
|
258
272
|
expect(result.relations.items).toHaveLength(2);
|
|
259
273
|
});
|
|
260
|
-
it('should open nodes filtered (only internal relations)', async () => {
|
|
261
|
-
const result = await callTool(client, 'open_nodes_filtered', {
|
|
262
|
-
names: ['B', 'C']
|
|
263
|
-
});
|
|
264
|
-
expect(result.entities.items).toHaveLength(2);
|
|
265
|
-
expect(result.relations.items).toHaveLength(0); // No relations between B and C
|
|
266
|
-
});
|
|
267
274
|
});
|
|
268
275
|
describe('Graph Traversal', () => {
|
|
269
276
|
beforeEach(async () => {
|
|
@@ -446,15 +453,21 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
446
453
|
const names = strict.items.map(e => e.name).sort();
|
|
447
454
|
expect(names).toEqual(['Island1', 'Island2']);
|
|
448
455
|
});
|
|
449
|
-
it('should validate graph and report violations', async () => {
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
+
it('should validate graph and report no violations on clean graph', async () => {
|
|
457
|
+
// Create a valid graph through the API
|
|
458
|
+
await callTool(client, 'create_entities', {
|
|
459
|
+
entities: [
|
|
460
|
+
{ name: 'Valid', entityType: 'Test', observations: [] },
|
|
461
|
+
{ name: 'Also_Valid', entityType: 'Test', observations: ['Short obs'] }
|
|
462
|
+
]
|
|
463
|
+
});
|
|
464
|
+
await callTool(client, 'create_relations', {
|
|
465
|
+
relations: [{ from: 'Valid', to: 'Also_Valid', relationType: 'refs' }]
|
|
466
|
+
});
|
|
456
467
|
const result = await callTool(client, 'validate_graph', {});
|
|
457
|
-
|
|
468
|
+
// Binary store enforces referential integrity — no missing entities possible
|
|
469
|
+
expect(result.missingEntities).toHaveLength(0);
|
|
470
|
+
expect(result.observationViolations).toHaveLength(0);
|
|
458
471
|
});
|
|
459
472
|
});
|
|
460
473
|
describe('Sequential Thinking', () => {
|
|
@@ -620,12 +633,16 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
620
633
|
{ name: 'Gamma', entityType: 'Letter', observations: ['Third letter'] }
|
|
621
634
|
]);
|
|
622
635
|
});
|
|
623
|
-
it('should
|
|
636
|
+
it('should use llmrank as default sort when sortBy is omitted', async () => {
|
|
624
637
|
const result = await callTool(client, 'search_nodes', {
|
|
625
638
|
query: 'Letter'
|
|
626
639
|
});
|
|
640
|
+
// With llmrank default, all entities returned (order varies due to random tiebreak)
|
|
627
641
|
const names = result.entities.items.map(e => e.name);
|
|
628
|
-
expect(names).
|
|
642
|
+
expect(names).toHaveLength(3);
|
|
643
|
+
expect(names).toContain('Alpha');
|
|
644
|
+
expect(names).toContain('Beta');
|
|
645
|
+
expect(names).toContain('Gamma');
|
|
629
646
|
});
|
|
630
647
|
it('should sort by name ascending', async () => {
|
|
631
648
|
const result = await callTool(client, 'search_nodes', {
|
|
@@ -689,12 +706,16 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
689
706
|
{ name: 'Monkey', entityType: 'Animal', observations: ['Clever'] }
|
|
690
707
|
]);
|
|
691
708
|
});
|
|
692
|
-
it('should
|
|
709
|
+
it('should use llmrank as default sort when sortBy is omitted', async () => {
|
|
693
710
|
const result = await callTool(client, 'get_entities_by_type', {
|
|
694
711
|
entityType: 'Animal'
|
|
695
712
|
});
|
|
713
|
+
// With llmrank default, all entities returned (order varies due to random tiebreak)
|
|
696
714
|
const names = result.items.map(e => e.name);
|
|
697
|
-
expect(names).
|
|
715
|
+
expect(names).toHaveLength(3);
|
|
716
|
+
expect(names).toContain('Zebra');
|
|
717
|
+
expect(names).toContain('Aardvark');
|
|
718
|
+
expect(names).toContain('Monkey');
|
|
698
719
|
});
|
|
699
720
|
it('should sort by name ascending (default for name)', async () => {
|
|
700
721
|
const result = await callTool(client, 'get_entities_by_type', {
|
|
@@ -742,10 +763,14 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
742
763
|
{ name: 'Orphan_M', entityType: 'Orphan', observations: ['Isolated'] }
|
|
743
764
|
]);
|
|
744
765
|
});
|
|
745
|
-
it('should
|
|
766
|
+
it('should use llmrank as default sort when sortBy is omitted', async () => {
|
|
746
767
|
const result = await callTool(client, 'get_orphaned_entities', {});
|
|
768
|
+
// With llmrank default, all entities returned (order varies due to random tiebreak)
|
|
747
769
|
const names = result.items.map(e => e.name);
|
|
748
|
-
expect(names).
|
|
770
|
+
expect(names).toHaveLength(3);
|
|
771
|
+
expect(names).toContain('Orphan_Z');
|
|
772
|
+
expect(names).toContain('Orphan_A');
|
|
773
|
+
expect(names).toContain('Orphan_M');
|
|
749
774
|
});
|
|
750
775
|
it('should sort by name ascending', async () => {
|
|
751
776
|
const result = await callTool(client, 'get_orphaned_entities', {
|
|
@@ -807,12 +832,12 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
807
832
|
]
|
|
808
833
|
});
|
|
809
834
|
});
|
|
810
|
-
it('should
|
|
835
|
+
it('should use llmrank as default sort when sortBy is omitted', async () => {
|
|
811
836
|
const result = await callTool(client, 'get_neighbors', {
|
|
812
837
|
entityName: 'Hub'
|
|
813
838
|
});
|
|
839
|
+
// With llmrank default, all neighbors returned (order varies due to random tiebreak)
|
|
814
840
|
expect(result.items).toHaveLength(3);
|
|
815
|
-
// Just verify all neighbors are present
|
|
816
841
|
const names = result.items.map(n => n.name);
|
|
817
842
|
expect(names).toContain('Neighbor_Z');
|
|
818
843
|
expect(names).toContain('Neighbor_A');
|
|
@@ -915,5 +940,91 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
915
940
|
expect(names).toEqual(sortedNames);
|
|
916
941
|
});
|
|
917
942
|
});
|
|
943
|
+
describe('pagerank sorting', () => {
|
|
944
|
+
it('should sort by pagerank (structural rank)', async () => {
|
|
945
|
+
// Build a star graph: Hub -> A, Hub -> B, Hub -> C
|
|
946
|
+
// A, B, C are dangling nodes
|
|
947
|
+
await callTool(client, 'create_entities', {
|
|
948
|
+
entities: [
|
|
949
|
+
{ name: 'Hub', entityType: 'Node', observations: ['Central node'] },
|
|
950
|
+
{ name: 'LeafA', entityType: 'Node', observations: ['Leaf A'] },
|
|
951
|
+
{ name: 'LeafB', entityType: 'Node', observations: ['Leaf B'] },
|
|
952
|
+
{ name: 'LeafC', entityType: 'Node', observations: ['Leaf C'] },
|
|
953
|
+
]
|
|
954
|
+
});
|
|
955
|
+
await callTool(client, 'create_relations', {
|
|
956
|
+
relations: [
|
|
957
|
+
{ from: 'Hub', to: 'LeafA', relationType: 'LINKS' },
|
|
958
|
+
{ from: 'Hub', to: 'LeafB', relationType: 'LINKS' },
|
|
959
|
+
{ from: 'Hub', to: 'LeafC', relationType: 'LINKS' },
|
|
960
|
+
]
|
|
961
|
+
});
|
|
962
|
+
// Sort by pagerank descending (default for ranks)
|
|
963
|
+
const result = await callTool(client, 'search_nodes', {
|
|
964
|
+
query: 'Node',
|
|
965
|
+
sortBy: 'pagerank'
|
|
966
|
+
});
|
|
967
|
+
// All entities should be returned
|
|
968
|
+
expect(result.entities.items).toHaveLength(4);
|
|
969
|
+
// With structural rank, the leaves should rank higher than the hub
|
|
970
|
+
// because they receive visits from Hub's walks.
|
|
971
|
+
// We just verify the sort works and returns all entities.
|
|
972
|
+
const names = result.entities.items.map(e => e.name);
|
|
973
|
+
expect(names).toContain('Hub');
|
|
974
|
+
expect(names).toContain('LeafA');
|
|
975
|
+
expect(names).toContain('LeafB');
|
|
976
|
+
expect(names).toContain('LeafC');
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
describe('llmrank sorting', () => {
|
|
980
|
+
it('should sort by llmrank (walker visits)', async () => {
|
|
981
|
+
await callTool(client, 'create_entities', {
|
|
982
|
+
entities: [
|
|
983
|
+
{ name: 'Hot', entityType: 'Test', observations: ['Frequently accessed'] },
|
|
984
|
+
{ name: 'Cold', entityType: 'Test', observations: ['Rarely accessed'] },
|
|
985
|
+
]
|
|
986
|
+
});
|
|
987
|
+
// Access 'Hot' multiple times via open_nodes (which increments walker visits)
|
|
988
|
+
await callTool(client, 'open_nodes', { names: ['Hot'] });
|
|
989
|
+
await callTool(client, 'open_nodes', { names: ['Hot'] });
|
|
990
|
+
await callTool(client, 'open_nodes', { names: ['Hot'] });
|
|
991
|
+
await callTool(client, 'open_nodes', { names: ['Hot'] });
|
|
992
|
+
await callTool(client, 'open_nodes', { names: ['Hot'] });
|
|
993
|
+
// Access 'Cold' just once
|
|
994
|
+
await callTool(client, 'open_nodes', { names: ['Cold'] });
|
|
995
|
+
// Sort by llmrank descending
|
|
996
|
+
const result = await callTool(client, 'search_nodes', {
|
|
997
|
+
query: 'Test',
|
|
998
|
+
sortBy: 'llmrank'
|
|
999
|
+
});
|
|
1000
|
+
// 'Hot' should rank higher than 'Cold' due to more walker visits
|
|
1001
|
+
expect(result.entities.items).toHaveLength(2);
|
|
1002
|
+
expect(result.entities.items[0].name).toBe('Hot');
|
|
1003
|
+
expect(result.entities.items[1].name).toBe('Cold');
|
|
1004
|
+
});
|
|
1005
|
+
it('should fall back to pagerank on llmrank tie', async () => {
|
|
1006
|
+
// Create entities with no prior walker visits
|
|
1007
|
+
await callTool(client, 'create_entities', {
|
|
1008
|
+
entities: [
|
|
1009
|
+
{ name: 'Center', entityType: 'Fallback', observations: ['Hub'] },
|
|
1010
|
+
{ name: 'Spoke', entityType: 'Fallback', observations: ['Leaf'] },
|
|
1011
|
+
]
|
|
1012
|
+
});
|
|
1013
|
+
// Create a relation so structural rank differs
|
|
1014
|
+
await callTool(client, 'create_relations', {
|
|
1015
|
+
relations: [{ from: 'Center', to: 'Spoke', relationType: 'POINTS_TO' }]
|
|
1016
|
+
});
|
|
1017
|
+
// Both have 0 walker visits (tie), so llmrank should fall back to pagerank
|
|
1018
|
+
const result = await callTool(client, 'search_nodes', {
|
|
1019
|
+
query: 'Fallback',
|
|
1020
|
+
sortBy: 'llmrank'
|
|
1021
|
+
});
|
|
1022
|
+
// Just verify we get both entities (ordering depends on structural rank + random tiebreak)
|
|
1023
|
+
expect(result.entities.items).toHaveLength(2);
|
|
1024
|
+
const names = result.entities.items.map(e => e.name);
|
|
1025
|
+
expect(names).toContain('Center');
|
|
1026
|
+
expect(names).toContain('Spoke');
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
918
1029
|
});
|
|
919
1030
|
});
|
package/native/binding.c
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* N-API binding for memoryfile
|
|
3
|
+
*
|
|
4
|
+
* Exposes the C memoryfile allocator to Node.js.
|
|
5
|
+
* Each MemoryFile handle is wrapped in a pointerless external.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#define NAPI_VERSION 8
|
|
9
|
+
#include <node_api.h>
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#include <string.h>
|
|
12
|
+
#include "memoryfile.h"
|
|
13
|
+
|
|
14
|
+
/* =========================================================================
|
|
15
|
+
* Helpers
|
|
16
|
+
* ========================================================================= */
|
|
17
|
+
|
|
18
|
+
#define NAPI_CALL(call) \
|
|
19
|
+
do { \
|
|
20
|
+
napi_status status = (call); \
|
|
21
|
+
if (status != napi_ok) { \
|
|
22
|
+
napi_throw_error(env, NULL, "N-API call failed: " #call); \
|
|
23
|
+
return NULL; \
|
|
24
|
+
} \
|
|
25
|
+
} while (0)
|
|
26
|
+
|
|
27
|
+
static napi_value make_u64(napi_env env, u64 val) {
|
|
28
|
+
/* Use BigInt for u64 to avoid precision loss */
|
|
29
|
+
napi_value result;
|
|
30
|
+
NAPI_CALL(napi_create_bigint_uint64(env, val, &result));
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static u64 get_u64(napi_env env, napi_value val) {
|
|
35
|
+
bool lossless;
|
|
36
|
+
uint64_t result;
|
|
37
|
+
napi_get_value_bigint_uint64(env, val, &result, &lossless);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static void mf_release(napi_env env, void *data, void *hint) {
|
|
42
|
+
(void)env; (void)hint;
|
|
43
|
+
memfile_t *mf = (memfile_t*)data;
|
|
44
|
+
if (mf) {
|
|
45
|
+
memfile_close(mf); /* no-op if already closed */
|
|
46
|
+
mf->mmap_base = NULL;
|
|
47
|
+
mf->header = NULL;
|
|
48
|
+
free(mf);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static memfile_t *unwrap_mf(napi_env env, napi_value val) {
|
|
53
|
+
memfile_t *mf;
|
|
54
|
+
napi_get_value_external(env, val, (void**)&mf);
|
|
55
|
+
return mf;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* =========================================================================
|
|
59
|
+
* memfile_open(path: string, initialSize: number) => external
|
|
60
|
+
* ========================================================================= */
|
|
61
|
+
|
|
62
|
+
static napi_value n_memfile_open(napi_env env, napi_callback_info info) {
|
|
63
|
+
size_t argc = 2;
|
|
64
|
+
napi_value argv[2];
|
|
65
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
66
|
+
|
|
67
|
+
/* Get path string */
|
|
68
|
+
char path[4096];
|
|
69
|
+
size_t path_len;
|
|
70
|
+
NAPI_CALL(napi_get_value_string_utf8(env, argv[0], path, sizeof(path), &path_len));
|
|
71
|
+
|
|
72
|
+
/* Get initial size */
|
|
73
|
+
uint32_t initial_size;
|
|
74
|
+
NAPI_CALL(napi_get_value_uint32(env, argv[1], &initial_size));
|
|
75
|
+
|
|
76
|
+
memfile_t *mf = memfile_open(path, (size_t)initial_size);
|
|
77
|
+
if (!mf) {
|
|
78
|
+
napi_throw_error(env, NULL, "memfile_open failed");
|
|
79
|
+
return NULL;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
napi_value result;
|
|
83
|
+
NAPI_CALL(napi_create_external(env, mf, mf_release, NULL, &result));
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* =========================================================================
|
|
88
|
+
* memfile_close(handle: external) => void
|
|
89
|
+
* ========================================================================= */
|
|
90
|
+
|
|
91
|
+
static napi_value n_memfile_close(napi_env env, napi_callback_info info) {
|
|
92
|
+
size_t argc = 1;
|
|
93
|
+
napi_value argv[1];
|
|
94
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
95
|
+
|
|
96
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
97
|
+
memfile_close(mf);
|
|
98
|
+
|
|
99
|
+
return NULL;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* =========================================================================
|
|
103
|
+
* memfile_sync(handle: external) => void
|
|
104
|
+
* ========================================================================= */
|
|
105
|
+
|
|
106
|
+
static napi_value n_memfile_sync(napi_env env, napi_callback_info info) {
|
|
107
|
+
size_t argc = 1;
|
|
108
|
+
napi_value argv[1];
|
|
109
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
110
|
+
|
|
111
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
112
|
+
memfile_sync(mf);
|
|
113
|
+
|
|
114
|
+
return NULL;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* =========================================================================
|
|
118
|
+
* memfile_alloc(handle: external, size: bigint) => bigint (offset)
|
|
119
|
+
* ========================================================================= */
|
|
120
|
+
|
|
121
|
+
static napi_value n_memfile_alloc(napi_env env, napi_callback_info info) {
|
|
122
|
+
size_t argc = 2;
|
|
123
|
+
napi_value argv[2];
|
|
124
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
125
|
+
|
|
126
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
127
|
+
u64 size = get_u64(env, argv[1]);
|
|
128
|
+
|
|
129
|
+
u64 offset = memfile_alloc(mf, size);
|
|
130
|
+
return make_u64(env, offset);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* =========================================================================
|
|
134
|
+
* memfile_free(handle: external, offset: bigint) => void
|
|
135
|
+
* ========================================================================= */
|
|
136
|
+
|
|
137
|
+
static napi_value n_memfile_free(napi_env env, napi_callback_info info) {
|
|
138
|
+
size_t argc = 2;
|
|
139
|
+
napi_value argv[2];
|
|
140
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
141
|
+
|
|
142
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
143
|
+
u64 offset = get_u64(env, argv[1]);
|
|
144
|
+
|
|
145
|
+
memfile_free(mf, offset);
|
|
146
|
+
|
|
147
|
+
return NULL;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* =========================================================================
|
|
151
|
+
* memfile_coalesce(handle: external) => void
|
|
152
|
+
* ========================================================================= */
|
|
153
|
+
|
|
154
|
+
static napi_value n_memfile_coalesce(napi_env env, napi_callback_info info) {
|
|
155
|
+
size_t argc = 1;
|
|
156
|
+
napi_value argv[1];
|
|
157
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
158
|
+
|
|
159
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
160
|
+
memfile_coalesce(mf);
|
|
161
|
+
|
|
162
|
+
return NULL;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* =========================================================================
|
|
166
|
+
* memfile_read(handle: external, offset: bigint, length: bigint) => Buffer
|
|
167
|
+
* ========================================================================= */
|
|
168
|
+
|
|
169
|
+
static napi_value n_memfile_read(napi_env env, napi_callback_info info) {
|
|
170
|
+
size_t argc = 3;
|
|
171
|
+
napi_value argv[3];
|
|
172
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
173
|
+
|
|
174
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
175
|
+
u64 offset = get_u64(env, argv[1]);
|
|
176
|
+
u64 len = get_u64(env, argv[2]);
|
|
177
|
+
|
|
178
|
+
/* Create a Node Buffer and copy data into it */
|
|
179
|
+
void *buf_data;
|
|
180
|
+
napi_value result;
|
|
181
|
+
NAPI_CALL(napi_create_buffer(env, (size_t)len, &buf_data, &result));
|
|
182
|
+
|
|
183
|
+
if (memfile_read(mf, offset, buf_data, len) < 0) {
|
|
184
|
+
napi_throw_error(env, NULL, "memfile_read: offset/length out of bounds");
|
|
185
|
+
return NULL;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* =========================================================================
|
|
192
|
+
* memfile_write(handle: external, offset: bigint, data: Buffer) => void
|
|
193
|
+
* ========================================================================= */
|
|
194
|
+
|
|
195
|
+
static napi_value n_memfile_write(napi_env env, napi_callback_info info) {
|
|
196
|
+
size_t argc = 3;
|
|
197
|
+
napi_value argv[3];
|
|
198
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
199
|
+
|
|
200
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
201
|
+
u64 offset = get_u64(env, argv[1]);
|
|
202
|
+
|
|
203
|
+
/* Get Buffer data */
|
|
204
|
+
void *buf_data;
|
|
205
|
+
size_t buf_len;
|
|
206
|
+
NAPI_CALL(napi_get_buffer_info(env, argv[2], &buf_data, &buf_len));
|
|
207
|
+
|
|
208
|
+
if (memfile_write(mf, offset, buf_data, buf_len) < 0) {
|
|
209
|
+
napi_throw_error(env, NULL, "memfile_write: offset/length out of bounds");
|
|
210
|
+
return NULL;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return NULL;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* =========================================================================
|
|
217
|
+
* memfile_lock_shared(handle: external) => void
|
|
218
|
+
* ========================================================================= */
|
|
219
|
+
|
|
220
|
+
static napi_value n_memfile_lock_shared(napi_env env, napi_callback_info info) {
|
|
221
|
+
size_t argc = 1;
|
|
222
|
+
napi_value argv[1];
|
|
223
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
224
|
+
|
|
225
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
226
|
+
if (memfile_lock_shared(mf) < 0) {
|
|
227
|
+
napi_throw_error(env, NULL, "memfile_lock_shared failed");
|
|
228
|
+
return NULL;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return NULL;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* =========================================================================
|
|
235
|
+
* memfile_lock_exclusive(handle: external) => void
|
|
236
|
+
* ========================================================================= */
|
|
237
|
+
|
|
238
|
+
static napi_value n_memfile_lock_exclusive(napi_env env, napi_callback_info info) {
|
|
239
|
+
size_t argc = 1;
|
|
240
|
+
napi_value argv[1];
|
|
241
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
242
|
+
|
|
243
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
244
|
+
if (memfile_lock_exclusive(mf) < 0) {
|
|
245
|
+
napi_throw_error(env, NULL, "memfile_lock_exclusive failed");
|
|
246
|
+
return NULL;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return NULL;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* =========================================================================
|
|
253
|
+
* memfile_unlock(handle: external) => void
|
|
254
|
+
* ========================================================================= */
|
|
255
|
+
|
|
256
|
+
static napi_value n_memfile_unlock(napi_env env, napi_callback_info info) {
|
|
257
|
+
size_t argc = 1;
|
|
258
|
+
napi_value argv[1];
|
|
259
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
260
|
+
|
|
261
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
262
|
+
if (memfile_unlock(mf) < 0) {
|
|
263
|
+
napi_throw_error(env, NULL, "memfile_unlock failed");
|
|
264
|
+
return NULL;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return NULL;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* =========================================================================
|
|
271
|
+
* memfile_stats(handle: external) => { fileSize, allocated, freeListHead }
|
|
272
|
+
* ========================================================================= */
|
|
273
|
+
|
|
274
|
+
static napi_value n_memfile_stats(napi_env env, napi_callback_info info) {
|
|
275
|
+
size_t argc = 1;
|
|
276
|
+
napi_value argv[1];
|
|
277
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
278
|
+
|
|
279
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
280
|
+
|
|
281
|
+
napi_value result;
|
|
282
|
+
NAPI_CALL(napi_create_object(env, &result));
|
|
283
|
+
|
|
284
|
+
napi_value v;
|
|
285
|
+
NAPI_CALL(napi_create_bigint_uint64(env, mf->header->file_size, &v));
|
|
286
|
+
NAPI_CALL(napi_set_named_property(env, result, "fileSize", v));
|
|
287
|
+
|
|
288
|
+
NAPI_CALL(napi_create_bigint_uint64(env, mf->header->allocated, &v));
|
|
289
|
+
NAPI_CALL(napi_set_named_property(env, result, "allocated", v));
|
|
290
|
+
|
|
291
|
+
NAPI_CALL(napi_create_bigint_uint64(env, mf->header->free_list_head, &v));
|
|
292
|
+
NAPI_CALL(napi_set_named_property(env, result, "freeListHead", v));
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* =========================================================================
|
|
298
|
+
* memfile_refresh(handle: external) => void
|
|
299
|
+
* ========================================================================= */
|
|
300
|
+
|
|
301
|
+
static napi_value n_memfile_refresh(napi_env env, napi_callback_info info) {
|
|
302
|
+
size_t argc = 1;
|
|
303
|
+
napi_value argv[1];
|
|
304
|
+
NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
305
|
+
|
|
306
|
+
memfile_t *mf = unwrap_mf(env, argv[0]);
|
|
307
|
+
if (memfile_refresh(mf) < 0) {
|
|
308
|
+
napi_throw_error(env, NULL, "memfile_refresh failed");
|
|
309
|
+
return NULL;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return NULL;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* =========================================================================
|
|
316
|
+
* Module init
|
|
317
|
+
* ========================================================================= */
|
|
318
|
+
|
|
319
|
+
#define EXPORT_FN(name, fn) do { \
|
|
320
|
+
napi_value _fn; \
|
|
321
|
+
napi_create_function(env, name, NAPI_AUTO_LENGTH, fn, NULL, &_fn); \
|
|
322
|
+
napi_set_named_property(env, exports, name, _fn); \
|
|
323
|
+
} while(0)
|
|
324
|
+
|
|
325
|
+
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {
|
|
326
|
+
EXPORT_FN("open", n_memfile_open);
|
|
327
|
+
EXPORT_FN("close", n_memfile_close);
|
|
328
|
+
EXPORT_FN("sync", n_memfile_sync);
|
|
329
|
+
EXPORT_FN("alloc", n_memfile_alloc);
|
|
330
|
+
EXPORT_FN("free", n_memfile_free);
|
|
331
|
+
EXPORT_FN("coalesce", n_memfile_coalesce);
|
|
332
|
+
EXPORT_FN("read", n_memfile_read);
|
|
333
|
+
EXPORT_FN("write", n_memfile_write);
|
|
334
|
+
EXPORT_FN("lockShared", n_memfile_lock_shared);
|
|
335
|
+
EXPORT_FN("lockExclusive", n_memfile_lock_exclusive);
|
|
336
|
+
EXPORT_FN("unlock", n_memfile_unlock);
|
|
337
|
+
EXPORT_FN("stats", n_memfile_stats);
|
|
338
|
+
EXPORT_FN("refresh", n_memfile_refresh);
|
|
339
|
+
return exports;
|
|
340
|
+
}
|