Skip to content

Commit

Permalink
feat(gems): persist gems in kumavis-store
Browse files Browse the repository at this point in the history
  • Loading branch information
kumavis committed Sep 7, 2024
1 parent d9777a6 commit 5b42e0b
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 98 deletions.
1 change: 0 additions & 1 deletion packages/gems/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"./package.json": "./package.json"
},
"scripts": {
"start": "node --expose-gc ./test/example.js",
"test": "ava",
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
"test:xs": "exit 0",
Expand Down
110 changes: 96 additions & 14 deletions packages/gems/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

export const util = { noop, never, delay };

const getRandomId = () => Math.random().toString(36).slice(2);

/**
* @template TBootstrap
* @param {string} name
Expand Down Expand Up @@ -58,14 +60,16 @@ const makePersistenceNode = () => {
},
set(newValue) {
if (typeof newValue !== 'string') {
throw new Error('persistence node expected string');
throw new Error(
`persistence node expected string (got "${typeof newValue}")`,
);
}
value = newValue;
},
};
};

const makeWakeController = ({ name, makeFacet, persistenceNode }) => {
const makeWakeController = ({ name, makeFacet }) => {
let isAwake = false;
let target;
let currentFacetId;
Expand Down Expand Up @@ -98,7 +102,10 @@ const makeWakeController = ({ name, makeFacet, persistenceNode }) => {
const facetId = Math.random().toString(36).slice(2);
// simulate startup process
await delay(200);
const facet = await makeFacet({ persistenceNode, facetId });
const facet = await makeFacet({
// for debugging:
facetId,
});
target = new WeakRef(facet);
currentFacetId = facetId;
registry.register(facet, facetId);
Expand Down Expand Up @@ -153,17 +160,92 @@ const makeWrapper = (name, wakeController, methodNames) => {
);
};

export const makeGem = ({ name, makeFacet, methodNames }) => {
console.log(`gem:${name} created`);
const makeGemFactory = ({ gemController }) => {
return ({ name, makeFacet, methodNames }) => {
const gemId = `gem:${getRandomId()}`;
console.log(`${gemId} created ("${name}")`);

const gemLookup = gemController.getLookup();
const persistenceNode = makePersistenceNode();
// we wrap this here to avoid passing things to the wake controller
// the wake controller adds little of value as "endowments"
const makeFacetWithEndowments = async endowments => {
return makeFacet({
...endowments,
persistenceNode,
gemLookup,
});
};
const wakeController = makeWakeController({
name,
makeFacet: makeFacetWithEndowments,
});
const wrapper = makeWrapper(name, wakeController, methodNames);
const target = Far(`${gemId}`, wrapper);
gemController.register(gemId, target);

return { target, wakeController };
};
};

const persistenceNode = makePersistenceNode();
const wakeController = makeWakeController({
name,
makeFacet,
persistenceNode,
});
const wrapper = makeWrapper(name, wakeController, methodNames);
const target = Far(`gem:${name}`, wrapper);
const makeGemController = () => {
const gemIdToGem = new Map();
const gemToGemId = new WeakMap();

return { target, wakeController };
const getGemById = gemId => {
return gemIdToGem.get(gemId);
};
const getGemId = gem => {
// if (!gemToGemId.has(gem)) {
// throw new Error(`Gem not found in lookup ("${gem}")`);
// }
// return gemToGemId.get(gem);

// this is a hack to get the gem id from the remote ref
// this is prolly not safe or something
// some identity discontinuity happening with the first technique
const str = String(gem);
const startIndex = str.indexOf('Alleged: ');
const endIndex = str.indexOf(']');
if (startIndex === -1 || endIndex === -1) {
throw new Error(`Could not find gem id in remote ref ("${str}")`);
}
const gemId = str.slice(startIndex + 9, endIndex);
if (!gemId) {
throw new Error('Gem id was empty');
}
if (!gemId.startsWith('gem:')) {
throw new Error('Gem id did not start with gem:');
}
return gemId;
};
const register = (gemId, gem) => {
if (gemIdToGem.has(gemId)) {
throw new Error(`Gem id already registered ("${gemId}")`);
}
console.log(`${gemId} registered: ${gem}`);
gemIdToGem.set(gemId, gem);
gemToGemId.set(gem, gemId);
};

return {
register,
getGemById,
getGemId,
getLookup() {
return {
getGemById,
getGemId,
};
},
};
};

export const makeKernel = () => {
const gemController = makeGemController();
const makeGem = makeGemFactory({ gemController });
return {
makeGem,
gemController,
};
};
56 changes: 53 additions & 3 deletions packages/gems/src/kumavis-store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
export const makeKumavisStore = async ({ persistenceNode }, initState) => {
const marshall = state => state;
const unmarshall = state => state;
const walkJson = (obj, handler) => {
// Loop through each key in the object
for (const key in obj) {
// Check if the key belongs to the object itself (not inherited)
if (Reflect.has(obj, key)) {
// Call the handler function with the current key and value
handler(obj, key, obj[key]);
// If the value is an object (and not null), recurse into it
if (typeof obj[key] === 'object' && obj[key] !== null) {
walkJson(obj[key], handler);
}
}
}
};

const isRemoteRef = ref =>
typeof ref === 'object' &&
!Array.isArray(ref) &&
String(ref).includes('Alleged:');

export const makeKumavisStore = async (
{ persistenceNode, gemLookup },
initState,
) => {
// turns gemRefs into prefixed strings
// and escapes ordinary strings
const marshall = state => {
walkJson(state, (parent, key, value) => {
if (typeof value === 'string') {
parent[key] = `string:${value}`;
} else if (isRemoteRef(value)) {
const gemId = gemLookup.getGemId(value);
parent[key] = gemId;
}
});
return state;
};
// turns prefixed strings back into strings
// and looks up gemRefs by id
const unmarshall = state => {
walkJson(state, (parent, key, value) => {
if (typeof value === 'string') {
if (value.startsWith('string:')) {
parent[key] = value.slice('string:'.length);
} else if (value.startsWith('gem:')) {
parent[key] = gemLookup.getGemById(value);
} else {
throw new Error('Unexpected unescaped string value in state');
}
}
});
return state;
};
const serialize = state => JSON.stringify(state);
const deserialize = string => JSON.parse(string);
const read = async () =>
Expand Down
77 changes: 0 additions & 77 deletions packages/gems/test/example.js

This file was deleted.

Loading

0 comments on commit 5b42e0b

Please sign in to comment.