Split up norman store

This commit is contained in:
Vincent Fiduccia 2019-08-14 16:55:45 -07:00
parent 461f741d47
commit eb039e8a35
No known key found for this signature in database
GPG Key ID: 2B29AD6BB2BB2582
7 changed files with 551 additions and 529 deletions

123
plugins/norman/actions.js Normal file
View File

@ -0,0 +1,123 @@
import { normalizeType } from './normalize';
import { SCHEMA } from '@/utils/types';
export default {
request({ dispatch }, opt) {
// @TODO queue/defer duplicate requests
opt.depaginate = opt.depaginate !== false;
return this.$axios(opt).then((res) => {
const out = res.data;
if ( res.status === 204 ) {
return;
}
if ( opt.depaginate ) {
// @TODO
/*
return new Promise((resolve, reject) => {
const next = res.pagination.next;
if (!next ) [
return resolve();
}
dispatch('request')
});
*/
}
Object.defineProperties(out, {
_status: { value: res.status },
_statusText: { value: res.statusText },
_headers: { value: res.headers },
_req: { value: res.request },
});
return out;
});
},
async loadSchemas({
getters, dispatch, commit, state
}) {
const res = await dispatch('findAll', { type: SCHEMA, opt: { url: '/v1/schemas', load: false } });
res.forEach((schema) => {
schema._id = normalizeType(schema.id);
// @TODO
// const links = schema.links;
// for ( const k in links ) {
// links[k] = links[k].replace('http://localhost:8989', 'https://localhost:8005');
// }
});
commit('registerType', SCHEMA);
commit('loadAll', { type: SCHEMA, data: res });
const all = getters.all(SCHEMA);
return all;
},
async findAll({ getters, commit, dispatch }, { type, opt }) {
console.log('Find All', type);
type = getters.normalizeType(type);
if ( getters['haveAll'](type) ) {
return getters['all'](type);
}
opt = opt || {};
opt.url = getters.urlFor(type, null, opt);
const res = await dispatch('request', opt);
if ( opt.load === false ) {
return res.data;
}
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
commit('loadAll', { type, data: res.data });
dispatch('watchType', { type });
const all = getters.all(type);
return all;
},
// opt:
// filter: Filter by fields, e.g. {field: value, anotherField: anotherValue} (default: none)
// limit: Number of reqords to return per page (default: 1000)
// sortBy: Sort by field
// sortOrder: asc or desc
// url: Use this specific URL instead of looking up the URL for the type/id. This should only be used for bootstraping schemas on startup.
// @TODO depaginate: If the response is paginated, retrieve all the pages. (default: true)
async find({ getters, commit, dispatch }, { type, id, opt }) {
const existing = getters.byId(type, id);
type = normalizeType(type);
if ( existing ) {
return existing;
}
opt = opt || {};
opt.url = getters.urlFor(type, id, opt);
const res = await dispatch('request', opt);
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
const neu = commit('load', res.data );
return neu;
},
};

120
plugins/norman/events.js Normal file
View File

@ -0,0 +1,120 @@
import { normalizeType } from './normalize';
const NO_WATCHING = ['schema'];
export default {
watchType({ dispatch, getters }, { type, revision }) {
if ( !process.client ) {
return;
}
type = normalizeType(type);
if ( NO_WATCHING.includes(type) ) {
return;
}
if ( typeof revision === 'undefined' ) {
revision = getters.nextResourceVersion(type);
}
return dispatch('ws.send', { resourceType: type, revision });
},
watchHaveAllTypes({ state, dispatch }) {
const promises = [];
const cache = state.types;
for ( const type in cache ) {
if ( cache[type].haveAll ) {
promises.push(dispatch('watchType', { type }));
}
}
return Promise.all(promises);
},
async 'ws.open'({ commit, dispatch }, event) {
commit('updateSocket', { status: 'connected', count: 0 });
console.log('WebSocket Opened');
const socket = event.currentTarget;
this.$socket = socket;
await dispatch('watchHaveAllTypes');
},
'ws.close'({ commit }, event) {
commit('updateSocket', { status: 'disconnected', count: 0 });
console.log('WebSocket Closed');
},
'ws.error'({ commit }, event) {
commit('updateSocket', { status: 'error' });
console.log('WebSocket Error');
},
'ws.reconnect'({ commit }, count) {
commit('updateSocket', { status: 'disconnected', count });
console.log(`WebSocket Reconnect Attempt #${ count }`);
},
'ws.reconnect-error'({ commit }) {
commit('updateSocket', { status: 'failed' });
console.error('WebSocket Reconnect Failed');
},
async 'ws.send'(ctx, obj) {
if ( this.$socket ) {
await this.$socket.send(JSON.stringify(obj));
} else {
console.log('Socket is not up yet');
}
},
'ws.ping'() {
console.log('WebSocket Ping');
},
'ws.resource.start'(_, msg) {
console.log('Resource start:', msg.resourceType);
},
'ws.resource.error'(_, msg) {
console.log('Resource error for', msg.resourceType, ':', msg.data.error);
},
'ws.resource.stop'(_, msg) {
console.log('Resource stop:', msg.resourceType);
},
'ws.resource.create'({ getters, commit }, { data }) {
const type = getters.normalizeType(data.type);
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
console.log('Load', data.type, data.id);
commit('load', data);
},
'ws.resource.change'({ getters, commit }, { data }) {
const type = getters.normalizeType(data.type);
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
console.log('Load', data.type, data.id);
commit('load', data);
},
'ws.resource.remove'({ getters, commit }, { data }) {
const type = getters.normalizeType(data.type);
if ( getters.hasType(type) ) {
commit('remove', { type, id: data.id });
}
},
};

View File

@ -0,0 +1,164 @@
import { normalizeType, KEY_FIELD_FOR } from './normalize';
import urlOptions from './urloptions';
import { COUNT, SCHEMA } from '@/utils/types';
export default {
all: (state, getters) => (type) => {
type = getters.normalizeType(type);
if ( getters.hasType(type) ) {
return state.types[type].list;
} else {
throw new Error(`All of ${ type } is not loaded`);
}
},
byId: (state, getters) => (type, id) => {
type = getters.normalizeType(type);
const entry = state.types[type];
if ( entry ) {
return entry.map.get(id);
}
},
schemaName: (state, getters) => (type) => {
type = getters.normalizeType(type);
const schemas = state.types[SCHEMA];
const keyField = KEY_FIELD_FOR[SCHEMA] || KEY_FIELD_FOR['default'];
const entry = schemas.list.find((x) => {
const thisOne = getters.normalizeType(x[keyField]);
return thisOne === type || thisOne.endsWith(`.${ type }`);
});
if ( entry ) {
return entry[keyField];
}
},
// Fuzzy is only for plugins/lookup, do not use in real code
schemaFor: (state, getters) => (type, fuzzy = false) => {
const schemas = state.types[SCHEMA];
type = normalizeType(type);
if ( !schemas ) {
throw new Error("Schemas aren't loaded yet");
}
const out = schemas.map.get(type);
if ( !out && fuzzy ) {
const close = getters.schemaName(type);
if ( close ) {
return getters.schemaFor(close);
}
}
return out;
},
hasType: (state, getters) => (type) => {
type = getters.normalizeType(type);
return !!state.types[type];
},
haveAll: (state, getters) => (type) => {
type = getters.normalizeType(type);
const entry = state.types[type];
if ( entry ) {
return entry.haveAll;
}
return false;
},
normalizeType: () => (type) => {
return normalizeType(type);
},
urlFor: (state, getters) => (type, id, opt) => {
opt = opt || {};
type = getters.normalizeType(type);
let url = opt.url;
if ( !url ) {
const schema = getters.schemaFor(type);
if ( !schema ) {
throw new Error(`Unknown schema for type: ${ type }`);
}
url = schema.links.collection;
if ( id ) {
url += `/${ id }`;
}
}
if ( !url.startsWith('/') && !url.startsWith('http') ) {
const baseUrl = state.config.baseUrl.replace(/\/$/, '');
url = `${ baseUrl }/${ url }`;
}
url = urlOptions(url, opt);
return url;
},
counts(state, getters) {
const obj = getters['all'](COUNT)[0].counts;
const out = Object.keys(obj).map((id) => {
const schema = getters['schemaFor'](id);
if ( !schema ) {
console.log('Unknown schema, id');
return null;
}
const attrs = schema.attributes || {};
const entry = obj[id];
return {
id,
label: attrs.kind,
group: attrs.group,
version: attrs.version,
namespaced: attrs.namespaced,
verbs: attrs.verbs,
count: entry.count,
byNamespace: entry.namespaces,
revision: entry.revision
};
});
return out.filter(x => !!x);
},
nextResourceVersion: (state, getters) => (type) => {
type = normalizeType(type);
const cache = state.types[type];
let revision = 0;
for ( const obj of cache.list ) {
if ( obj && obj.metadata ) {
const neu = parseInt(obj.metadata.resourceVersion, 10);
revision = Math.max(revision, neu);
}
}
if ( revision ) {
return revision + 1;
}
return null;
}
};

View File

@ -1,22 +1,14 @@
import Vue from 'vue';
import urlOptions from './urloptions';
import ResourceProxy from './resource-proxy';
import { clear, addObject, addObjects, removeObject } from '@/utils/array';
import { COUNT, SCHEMA } from '@/utils/types';
const KEY_FIELD_FOR = {
[SCHEMA]: '_id',
default: 'id',
};
const NO_WATCHING = ['schema'];
import actions from './actions';
import getters from './getters';
import mutations from './mutations';
import events from './events';
export const Norman = {
namespaced: true,
state() {
return {
config: { baseUrl: '/v1' },
config: { baseUrl: '' },
types: {},
socket: {
status: 'disconnected',
@ -25,508 +17,19 @@ export const Norman = {
};
},
getters: {
all: (state, getters) => (type) => {
type = getters.normalizeType(type);
if ( getters.hasType(type) ) {
return state.types[type].list;
} else {
throw new Error(`All of ${ type } is not loaded`);
}
},
byId: (state, getters) => (type, id) => {
type = getters.normalizeType(type);
const entry = state.types[type];
if ( entry ) {
return entry.map.get(id);
}
},
schemaName: (state, getters) => (type) => {
type = getters.normalizeType(type);
const schemas = state.types[SCHEMA];
const keyField = KEY_FIELD_FOR[SCHEMA] || KEY_FIELD_FOR['default'];
const entry = schemas.list.find((x) => {
const thisOne = getters.normalizeType(x[keyField]);
return thisOne === type || thisOne.endsWith(`.${ type }`);
});
if ( entry ) {
return entry[keyField];
}
},
// Fuzzy is only for plugins/lookup, do not use in real code
schemaFor: (state, getters) => (type, fuzzy = false) => {
const schemas = state.types[SCHEMA];
type = normalizeType(type);
if ( !schemas ) {
throw new Error("Schemas aren't loaded yet");
}
const out = schemas.map.get(type);
if ( !out && fuzzy ) {
const close = getters.schemaName(type);
if ( close ) {
return getters.schemaFor(close);
}
}
return out;
},
hasType: (state, getters) => (type) => {
type = getters.normalizeType(type);
return !!state.types[type];
},
haveAll: (state, getters) => (type) => {
type = getters.normalizeType(type);
const entry = state.types[type];
if ( entry ) {
return entry.haveAll;
}
return false;
},
normalizeType: () => (type) => {
return normalizeType(type);
},
urlFor: (state, getters) => (type, id, opt) => {
opt = opt || {};
type = getters.normalizeType(type);
let url = opt.url;
if ( !url ) {
const schema = getters.schemaFor(type);
if ( !schema ) {
throw new Error(`Unknown schema for type: ${ type }`);
}
url = schema.links.collection;
if ( id ) {
url += `/${ id }`;
}
}
if ( !url.startsWith('/') && !url.startsWith('http') ) {
const baseUrl = state.config.baseUrl.replace(/\/$/, '');
url = `${ baseUrl }/${ url }`;
}
url = urlOptions(url, opt);
return url;
},
counts(state, getters) {
const obj = getters['all'](COUNT)[0].counts;
const out = Object.keys(obj).map((id) => {
const schema = getters['schemaFor'](id);
if ( !schema ) {
console.log('Unknown schema, id');
return null;
}
const attrs = schema.attributes || {};
const entry = obj[id];
return {
id,
label: attrs.kind,
group: attrs.group,
version: attrs.version,
namespaced: attrs.namespaced,
verbs: attrs.verbs,
count: entry.count,
byNamespace: entry.namespaces,
revision: entry.revision
};
});
return out.filter(x => !!x);
},
nextResourceVersion: (state, getters) => (type) => {
type = normalizeType(type);
const cache = state.types[type];
let revision = 0;
for ( const obj of cache.list ) {
if ( obj && obj.metadata ) {
const neu = parseInt(obj.metadata.resourceVersion, 10);
revision = Math.max(revision, neu);
}
}
if ( revision ) {
return revision + 1;
}
return null;
}
},
mutations: {
updateSocket(state, obj) {
state.socket.status = obj.status;
state.socket.count = obj.count || 0;
},
rehydrateProxies(state) {
if ( !process.client ) {
return;
}
Object.keys(state.types).forEach((type) => {
const keyField = KEY_FIELD_FOR[type] || KEY_FIELD_FOR['default'];
const cache = state.types[type];
const map = new Map();
for ( let i = 0 ; i < cache.list.length ; i++ ) {
const proxy = proxyFor(cache.list[i]);
cache.list[i] = proxy;
map.set(proxy[keyField], proxy);
}
Vue.set(cache, 'map', map);
Vue.set(state.types, type, state.types[type]);
});
},
applyConfig(state, config) {
if ( !state.config ) {
state.config = {};
}
Object.assign(state.config, config);
},
registerType(state, type) {
if ( !state.types[type] ) {
const obj = {
list: [],
haveAll: false,
};
// Not enumerable so they don't get sent back to the client for SSR
Object.defineProperty(obj, 'map', { value: new Map() });
state.types[type] = obj;
}
},
loadAll(state, { type, data }) {
const cache = state.types[type];
const keyField = KEY_FIELD_FOR[type] || KEY_FIELD_FOR['default'];
clear(cache.list);
cache.map.clear();
const proxies = data.map(x => proxyFor(x));
addObjects(cache.list, proxies);
for ( let i = 0 ; i < data.length ; i++ ) {
cache.map.set(data[i][keyField], proxies[i]);
}
cache.haveAll = true;
},
load(state, resource) {
const type = normalizeType(resource.type);
const keyField = KEY_FIELD_FOR[type] || KEY_FIELD_FOR['default'];
const id = resource[keyField];
const cache = state.types[type];
const entry = cache.map.get(id);
if ( entry ) {
Object.assign(entry, resource);
} else {
const proxy = proxyFor(resource);
addObject(cache.list, proxy);
cache.map.set(id, proxy);
}
},
remove(state, { type, id }) {
type = normalizeType(type);
const entry = state.types[type];
if ( !entry ) {
return;
}
const obj = entry.map.get(id);
if ( obj ) {
removeObject(entry.list, obj);
entry.map.delete(id);
}
}
},
getters,
mutations,
actions: {
request({ dispatch }, opt) {
// @TODO queue/defer duplicate requests
opt.depaginate = opt.depaginate !== false;
return this.$axios(opt).then((res) => {
const out = res.data;
if ( res.status === 204 ) {
return;
}
if ( opt.depaginate ) {
// @TODO
/*
return new Promise((resolve, reject) => {
const next = res.pagination.next;
if (!next ) [
return resolve();
}
dispatch('request')
});
*/
}
Object.defineProperties(out, {
_status: { value: res.status },
_statusText: { value: res.statusText },
_headers: { value: res.headers },
_req: { value: res.request },
});
return out;
});
},
async loadSchemas({
getters, dispatch, commit, state
}) {
const res = await dispatch('findAll', { type: SCHEMA, opt: { url: '/v1/schemas', load: false } });
res.forEach((schema) => {
schema._id = normalizeType(schema.id);
// @TODO
// const links = schema.links;
// for ( const k in links ) {
// links[k] = links[k].replace('http://localhost:8989', 'https://localhost:8005');
// }
});
commit('registerType', SCHEMA);
commit('loadAll', { type: SCHEMA, data: res });
const all = getters.all(SCHEMA);
return all;
},
async findAll({ getters, commit, dispatch }, { type, opt }) {
console.log('Find All', type);
type = getters.normalizeType(type);
if ( getters['haveAll'](type) ) {
return getters['all'](type);
}
opt = opt || {};
opt.url = getters.urlFor(type, null, opt);
const res = await dispatch('request', opt);
if ( opt.load === false ) {
return res.data;
}
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
commit('loadAll', { type, data: res.data });
dispatch('watchType', { type });
const all = getters.all(type);
return all;
},
// opt:
// filter: Filter by fields, e.g. {field: value, anotherField: anotherValue} (default: none)
// limit: Number of reqords to return per page (default: 1000)
// sortBy: Sort by field
// sortOrder: asc or desc
// url: Use this specific URL instead of looking up the URL for the type/id. This should only be used for bootstraping schemas on startup.
// @TODO depaginate: If the response is paginated, retrieve all the pages. (default: true)
async find({ getters, commit, dispatch }, { type, id, opt }) {
const existing = getters.byId(type, id);
type = normalizeType(type);
if ( existing ) {
return existing;
}
opt = opt || {};
opt.url = getters.urlFor(type, id, opt);
const res = await dispatch('request', opt);
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
const neu = commit('load', res.data );
return neu;
},
watchType({ dispatch, getters }, { type, revision }) {
if ( !process.client ) {
return;
}
type = normalizeType(type);
if ( NO_WATCHING.includes(type) ) {
return;
}
if ( typeof revision === 'undefined' ) {
revision = getters.nextResourceVersion(type);
}
return dispatch('ws.send', { resourceType: type, revision });
},
watchHaveAllTypes({ state, dispatch }) {
const promises = [];
const cache = state.types;
for ( const type in cache ) {
if ( cache[type].haveAll ) {
promises.push(dispatch('watchType', { type }));
}
}
return Promise.all(promises);
},
async 'ws.open'({ commit, dispatch }, event) {
commit('updateSocket', { status: 'connected', count: 0 });
console.log('WebSocket Opened');
const socket = event.currentTarget;
this.$socket = socket;
await dispatch('watchHaveAllTypes');
},
'ws.close'({ commit }, event) {
commit('updateSocket', { status: 'disconnected', count: 0 });
console.log('WebSocket Closed');
},
'ws.error'({ commit }, event) {
commit('updateSocket', { status: 'error' });
console.log('WebSocket Error');
},
'ws.reconnect'({ commit }, count) {
commit('updateSocket', { status: 'disconnected', count });
console.log(`WebSocket Reconnect Attempt #${ count }`);
},
'ws.reconnect-error'({ commit }) {
commit('updateSocket', { status: 'failed' });
console.error('WebSocket Reconnect Failed');
},
async 'ws.send'(ctx, obj) {
if ( this.$socket ) {
await this.$socket.send(JSON.stringify(obj));
} else {
console.log('Socket is not up yet');
}
},
'ws.ping'() {
console.log('WebSocket Ping');
},
'ws.resource.start'(_, msg) {
console.log('Resource start:', msg.resourceType);
},
'ws.resource.error'(_, msg) {
console.log('Resource error for', msg.resourceType, ':', msg.data.error);
},
'ws.resource.stop'(_, msg) {
console.log('Resource stop:', msg.resourceType);
},
'ws.resource.create'({ getters, commit }, { data }) {
const type = getters.normalizeType(data.type);
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
console.log('Load', data.type, data.id);
commit('load', data);
},
'ws.resource.change'({ getters, commit }, { data }) {
const type = getters.normalizeType(data.type);
if ( !getters.hasType(type) ) {
commit('registerType', type);
}
console.log('Load', data.type, data.id);
commit('load', data);
},
'ws.resource.remove'({ getters, commit }, { data }) {
const type = getters.normalizeType(data.type);
if ( getters.hasType(type) ) {
commit('remove', { type, id: data.id });
}
},
...actions,
...events
},
};
export default (config = {}) => {
const namespace = config.namespace || '';
config.baseUrl = config.baseUrl || `/${ namespace }`;
// const preserveState = config.preserveState !== false;
return function(store) {
@ -534,23 +37,3 @@ export default (config = {}) => {
store.commit(`${ namespace }/applyConfig`, config);
};
};
function normalizeType(type) {
type = (type || '').toLowerCase();
return type;
}
function proxyFor(obj) {
return new Proxy(obj, {
get(target, name) {
const fn = ResourceProxy[name];
if ( fn ) {
return fn.apply(target);
}
return target[name];
},
});
}

120
plugins/norman/mutations.js Normal file
View File

@ -0,0 +1,120 @@
import Vue from 'vue';
import { normalizeType, KEY_FIELD_FOR } from './normalize';
import ResourceProxy from './resource-proxy';
import { addObject, addObjects, clear, removeObject } from '@/utils/array';
export default {
updateSocket(state, obj) {
state.socket.status = obj.status;
state.socket.count = obj.count || 0;
},
rehydrateProxies(state) {
if ( !process.client ) {
return;
}
Object.keys(state.types).forEach((type) => {
const keyField = KEY_FIELD_FOR[type] || KEY_FIELD_FOR['default'];
const cache = state.types[type];
const map = new Map();
for ( let i = 0 ; i < cache.list.length ; i++ ) {
const proxy = proxyFor(cache.list[i]);
cache.list[i] = proxy;
map.set(proxy[keyField], proxy);
}
Vue.set(cache, 'map', map);
Vue.set(state.types, type, state.types[type]);
});
},
applyConfig(state, config) {
if ( !state.config ) {
state.config = {};
}
Object.assign(state.config, config);
},
registerType(state, type) {
if ( !state.types[type] ) {
const obj = {
list: [],
haveAll: false,
};
// Not enumerable so they don't get sent back to the client for SSR
Object.defineProperty(obj, 'map', { value: new Map() });
state.types[type] = obj;
}
},
loadAll(state, { type, data }) {
const cache = state.types[type];
const keyField = KEY_FIELD_FOR[type] || KEY_FIELD_FOR['default'];
clear(cache.list);
cache.map.clear();
const proxies = data.map(x => proxyFor(x));
addObjects(cache.list, proxies);
for ( let i = 0 ; i < data.length ; i++ ) {
cache.map.set(data[i][keyField], proxies[i]);
}
cache.haveAll = true;
},
load(state, resource) {
const type = normalizeType(resource.type);
const keyField = KEY_FIELD_FOR[type] || KEY_FIELD_FOR['default'];
const id = resource[keyField];
const cache = state.types[type];
const entry = cache.map.get(id);
if ( entry ) {
Object.assign(entry, resource);
} else {
const proxy = proxyFor(resource);
addObject(cache.list, proxy);
cache.map.set(id, proxy);
}
},
remove(state, { type, id }) {
type = normalizeType(type);
const entry = state.types[type];
if ( !entry ) {
return;
}
const obj = entry.map.get(id);
if ( obj ) {
removeObject(entry.list, obj);
entry.map.delete(id);
}
}
};
function proxyFor(obj) {
return new Proxy(obj, {
get(target, name) {
const fn = ResourceProxy[name];
if ( fn ) {
return fn.apply(target);
}
return target[name];
},
});
}

View File

@ -0,0 +1,12 @@
import { SCHEMA } from '@/utils/types';
export const KEY_FIELD_FOR = {
[SCHEMA]: '_id',
default: 'id',
};
export function normalizeType(type) {
type = (type || '').toLowerCase();
return type;
}