mirror of
https://github.com/autismjs/monorepo.git
synced 2026-01-10 05:08:07 -05:00
feat: basic dom update
This commit is contained in:
@@ -208,7 +208,7 @@ export default class LevelDBAdapter implements BaseDBAdapter {
|
||||
reverse?: boolean;
|
||||
limit?: number;
|
||||
offset?: string;
|
||||
}): Promise<Any[]> {
|
||||
}): Promise<Post[]> {
|
||||
const predicate = (msg: Any) => {
|
||||
switch (msg.subtype) {
|
||||
case PostSubtype.Default:
|
||||
@@ -225,7 +225,7 @@ export default class LevelDBAdapter implements BaseDBAdapter {
|
||||
|
||||
const db = this.#indices.global.sublevel(MessageType[MessageType.Post]);
|
||||
|
||||
return this.#query(db, predicate, options);
|
||||
return this.#query(db, predicate, options) as Promise<Post[]>;
|
||||
}
|
||||
|
||||
async getPostsByUser(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type StateOptions = { [key: string]: BaseState };
|
||||
export type StateOptions = { [key: string]: Store };
|
||||
|
||||
export type RPC<Params = any> = {
|
||||
method: string;
|
||||
@@ -11,14 +11,13 @@ export type RPC<Params = any> = {
|
||||
|
||||
export type RPCHandler = (rpc: RPC) => void | Promise<void>;
|
||||
export type SubscriptionHandler = (
|
||||
prevState: Map<string, BaseState | any>,
|
||||
nextState: Map<string, BaseState | any>,
|
||||
prevState: Map<string, Store | any>,
|
||||
nextState: Map<string, Store | any>,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export default class BaseState {
|
||||
private states: Map<string, BaseState | any> = new Map();
|
||||
export default class Store {
|
||||
private states: Map<string, Store | any> = new Map();
|
||||
private handlers: Map<string, RPCHandler> = new Map();
|
||||
private subscriptions: SubscriptionHandler[] = [];
|
||||
|
||||
constructor(states?: StateOptions) {
|
||||
Object.entries(states || {}).forEach(([key, value]) => {
|
||||
@@ -26,7 +25,7 @@ export default class BaseState {
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string): BaseState | undefined {
|
||||
get<RetType = Store | undefined>(key: string): RetType {
|
||||
return this.states.get(key);
|
||||
}
|
||||
|
||||
@@ -35,8 +34,6 @@ export default class BaseState {
|
||||
}
|
||||
|
||||
async dispatch(rpc: RPC) {
|
||||
const prevState = new Map(this.states);
|
||||
|
||||
const handler = this.handlers.get(rpc.method);
|
||||
|
||||
if (handler) {
|
||||
@@ -44,19 +41,76 @@ export default class BaseState {
|
||||
}
|
||||
|
||||
for (const [, state] of this.states) {
|
||||
if (state instanceof BaseState) {
|
||||
if (state instanceof Store) {
|
||||
await state.dispatch(rpc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextState = this.states;
|
||||
export type Subscription<ValueType = any> =
|
||||
| {
|
||||
next?: (value: ValueType) => void;
|
||||
error?: (err: Error) => void;
|
||||
complete?: () => void;
|
||||
}
|
||||
| ((value: ValueType) => void);
|
||||
|
||||
for (const cb of this.subscriptions) {
|
||||
await cb(prevState, nextState);
|
||||
export class Observables<ObservableValue = any> {
|
||||
#state: ObservableValue;
|
||||
#error: Error | null = null;
|
||||
#subscriptions: Subscription[] = [];
|
||||
|
||||
get state() {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
set state(state: ObservableValue) {
|
||||
this.#state = state;
|
||||
for (const sub of this.#subscriptions) {
|
||||
if (typeof sub === 'function') {
|
||||
sub(state);
|
||||
} else if (typeof sub !== 'function' && sub.next) {
|
||||
sub.next(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(callback: SubscriptionHandler) {
|
||||
this.subscriptions.push(callback);
|
||||
get error(): Error | null {
|
||||
return this.#error;
|
||||
}
|
||||
|
||||
set error(error: Error) {
|
||||
this.#error = error;
|
||||
for (const sub of this.#subscriptions) {
|
||||
if (typeof sub !== 'function' && sub.error) {
|
||||
sub.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(state: ObservableValue) {
|
||||
this.#state = state;
|
||||
}
|
||||
|
||||
done() {
|
||||
for (const sub of this.#subscriptions) {
|
||||
if (typeof sub !== 'function' && sub.complete) {
|
||||
sub.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribe<ValueType = any>(subscription: Subscription<ValueType>) {
|
||||
const currIndex = this.#subscriptions.indexOf(subscription);
|
||||
if (currIndex === -1) {
|
||||
this.#subscriptions.push(subscription);
|
||||
}
|
||||
return () => {
|
||||
const index = this.#subscriptions.indexOf(subscription);
|
||||
if (index !== -1) {
|
||||
this.#subscriptions.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
87
packages/web/lib/ui.ts
Normal file
87
packages/web/lib/ui.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
interface CustomElementConstructor {
|
||||
new (): CustomElement;
|
||||
}
|
||||
export class CustomElement extends HTMLElement {
|
||||
css: string;
|
||||
html: string;
|
||||
|
||||
connectedCallback() {
|
||||
this.attachShadow({ mode: 'open' });
|
||||
const temp = document.createElement('template');
|
||||
temp.innerHTML = `<style>${this.css}</style>${this.html}`;
|
||||
this.shadowRoot?.appendChild(temp.content);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function html(htmlString: string) {
|
||||
const temp = document.createElement('template');
|
||||
temp.innerHTML = htmlString;
|
||||
return temp.content;
|
||||
}
|
||||
|
||||
export function register(name: string, el: CustomElementConstructor) {
|
||||
window.customElements.define(name, el);
|
||||
}
|
||||
|
||||
export const Q = (root: ShadowRoot | Element) => {
|
||||
return {
|
||||
find: (str: string) => E(root.querySelector(str)),
|
||||
findAll: (
|
||||
str: string,
|
||||
): Element[] & {
|
||||
patch: (
|
||||
result: any[],
|
||||
mapKeyFn: (data: any) => string,
|
||||
renderFn: (data: any) => Element | DocumentFragment,
|
||||
) => void;
|
||||
} => {
|
||||
const list = Array.prototype.map.call(
|
||||
root.querySelectorAll(str),
|
||||
E,
|
||||
) as Element[];
|
||||
|
||||
// @ts-ignore
|
||||
list.patch = (
|
||||
result: any[],
|
||||
mapKeyFn: (data: any) => string,
|
||||
renderFn: (data: any) => Element,
|
||||
) => {
|
||||
const max = Math.max(list.length, result.length);
|
||||
for (let i = 0; i < max; i++) {
|
||||
const data = result[i];
|
||||
const last = list[i];
|
||||
const lastKey = last?.getAttribute('key');
|
||||
const currKey = mapKeyFn(data);
|
||||
|
||||
if (!last && data) {
|
||||
root.append(renderFn(data));
|
||||
} else if (last && !data) {
|
||||
root.removeChild(last);
|
||||
} else if (lastKey !== currKey) {
|
||||
root.replaceChild(renderFn(data), last);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
return list;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const E = (el: Element | null) => {
|
||||
if (!el) return el;
|
||||
|
||||
return {
|
||||
content: (content: string) => {
|
||||
el.textContent = content;
|
||||
return el;
|
||||
},
|
||||
attr: (key: string, value: string) => {
|
||||
el.setAttribute(key, value);
|
||||
return el;
|
||||
},
|
||||
getAttribute: el.getAttribute.bind(el),
|
||||
};
|
||||
};
|
||||
33
packages/web/src/components/Post/index.ts
Normal file
33
packages/web/src/components/Post/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { CustomElement, Q, register } from '../../../lib/ui.ts';
|
||||
import { getStore } from '../../state';
|
||||
import { default as NodeStore } from '../../state/node.ts';
|
||||
|
||||
export default class Post extends CustomElement {
|
||||
css = `
|
||||
.post {
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
html = `
|
||||
<div class="post">
|
||||
<div id="creator"></div>
|
||||
<div id="content"></div>
|
||||
<div id="createdAt"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const store = getStore();
|
||||
const node = store.get<NodeStore>('node');
|
||||
const hash = this.dataset.hash!;
|
||||
const post = await node.getPost(hash);
|
||||
const q = Q(this.shadowRoot!);
|
||||
q.find('div#creator')!.content(post?.json.creator || '');
|
||||
q.find('div#content')!.content(post?.json.content || '');
|
||||
q.find('div#createdAt')!.content(post?.json.createdAt.toDateString() || '');
|
||||
}
|
||||
}
|
||||
|
||||
register('post-card', Post);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MessageType, Post, PostSubtype, ProofType } from '@autismjs/message';
|
||||
import { ECDSA } from '../../crypto/src';
|
||||
import State from './state';
|
||||
import { getStore } from './state';
|
||||
import App from './pages/App';
|
||||
|
||||
const ecdsa = new ECDSA();
|
||||
console.log('ecdsa', ecdsa);
|
||||
@@ -15,12 +16,11 @@ const p = new Post({
|
||||
console.log('post', p.json);
|
||||
|
||||
(async () => {
|
||||
const state = new State();
|
||||
const state = getStore();
|
||||
|
||||
console.log('state', state);
|
||||
|
||||
state.subscribe((prev, next) => {
|
||||
console.log(prev, next);
|
||||
});
|
||||
document.body.append(new App());
|
||||
|
||||
state.dispatch({
|
||||
method: 'node/check',
|
||||
|
||||
45
packages/web/src/pages/App/index.ts
Normal file
45
packages/web/src/pages/App/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { CustomElement, html, Q, register } from '../../../lib/ui.ts';
|
||||
import { getStore } from '../../state';
|
||||
import { default as NodeState } from '../../state/node.ts';
|
||||
import { Post as PostType } from '@autismjs/message';
|
||||
import '../../components/Post';
|
||||
|
||||
export default class App extends CustomElement {
|
||||
css = `
|
||||
.app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
`;
|
||||
|
||||
html = `
|
||||
<div class="app">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const state = getStore();
|
||||
const node = state.get<NodeState>('node');
|
||||
const app = this.shadowRoot!.querySelector('div.app')!;
|
||||
const q = Q(app);
|
||||
|
||||
node.posts.subscribe<PostType[]>((posts) => {
|
||||
const old = q.findAll('post-card');
|
||||
old.patch(
|
||||
posts,
|
||||
(post: PostType) => post.messageId,
|
||||
(post: PostType) =>
|
||||
html(`
|
||||
<post-card
|
||||
key="${post.messageId}"
|
||||
data-hash="${post.hash}"
|
||||
/>
|
||||
`),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
register('app-container', App);
|
||||
@@ -1,16 +1,10 @@
|
||||
import Node from './node.ts';
|
||||
import BaseState from '../../lib/state.ts';
|
||||
import Store from '../../lib/state.ts';
|
||||
|
||||
class State extends BaseState {
|
||||
constructor() {
|
||||
super({
|
||||
node: new Node(),
|
||||
});
|
||||
const store = new Store({
|
||||
node: new Node(),
|
||||
});
|
||||
|
||||
this.rpc('state/check', (rpc) => {
|
||||
console.log(rpc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default State;
|
||||
export const getStore = () => {
|
||||
return store;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import BaseState, { type StateOptions } from '../../lib/state.ts';
|
||||
import Store, { Observables, type StateOptions } from '../../lib/state.ts';
|
||||
import { Autism } from '@autismjs/protocol/src/services/browser.ts';
|
||||
import { Post } from '@autismjs/message';
|
||||
|
||||
export default class Node extends BaseState {
|
||||
export default class Node extends Store {
|
||||
node: Autism;
|
||||
wait: Promise<void>;
|
||||
posts = new Observables<Post[]>([]);
|
||||
|
||||
constructor(options?: StateOptions) {
|
||||
super(options);
|
||||
|
||||
const node = new Autism({
|
||||
bootstrap: [
|
||||
'/ip4/127.0.0.1/tcp/57575/ws/p2p/12D3KooWSoKnYV5idyrrJt3T6WM4eB6wcu58zUuy5bj7cEZNLwdm',
|
||||
],
|
||||
});
|
||||
|
||||
node.on('p2p:peer:discovery', (peer) => {
|
||||
// console.log('peer discovered', peer);
|
||||
});
|
||||
this.node = node;
|
||||
|
||||
node.on('p2p:peer:connect', (peer) => {
|
||||
console.log('peer connected', peer);
|
||||
@@ -22,20 +23,28 @@ export default class Node extends BaseState {
|
||||
|
||||
node.on('pubsub:message:success', (peer) => {
|
||||
console.log('pubsub:message:success', peer);
|
||||
this.updatePosts();
|
||||
});
|
||||
|
||||
node.on('sync:new_message', (peer) => {
|
||||
console.log('sync:new_message', peer);
|
||||
this.updatePosts();
|
||||
});
|
||||
|
||||
this.node = node;
|
||||
this.updatePosts();
|
||||
|
||||
this.wait = new Promise(async (r) => {
|
||||
await this.node.start();
|
||||
this.updatePosts();
|
||||
r();
|
||||
});
|
||||
|
||||
this.rpc('node/check', (rpc) => {
|
||||
console.log(rpc);
|
||||
});
|
||||
}
|
||||
|
||||
updatePosts = async () => {
|
||||
this.posts.state = await this.node.db.db.getPosts();
|
||||
};
|
||||
|
||||
getPost = async (hash: string): Promise<Post | null> => {
|
||||
return this.node.db.db.getMessage(hash) as Promise<Post | null>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Auti.sm</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-container"></div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user