Merge remote-tracking branch 'origin/master' into release/next-sys

This commit is contained in:
Philip Monk 2021-07-21 13:49:07 -04:00
commit d15d832ef4
No known key found for this signature in database
GPG Key ID: B66E1F02604E44EC
22 changed files with 197 additions and 268 deletions

View File

@ -26,6 +26,6 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script> <script src="/~landscape/js/session.js"></script>
<script src="/~btc/js/bundle/index.3e8bcc150ebd820dd3b2.js"></script> <script src="/~btc/js/bundle/index.02730169f3d73fd22950.js"></script>
</body> </body>
</html> </html>

View File

@ -5,8 +5,8 @@
/- glob, *resource /- glob, *resource
/+ default-agent, verb, dbug /+ default-agent, verb, dbug
|% |%
++ landscape-hash 0vrbiqe.v6al2.0b4jc.u9vp7.k1e0i ++ landscape-hash 0v4.56ejl.qq4js.h1i0m.t3e94.bg6qr
++ btc-wallet-hash 0v7.v4dng.o33qi.kc497.5jc02.ke5es ++ btc-wallet-hash 0v2.ifoe4.fbv35.aigir.66su4.fbspu
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ state-1 [%1 =globs:glob] +$ state-1 [%1 =globs:glob]
+$ all-states +$ all-states

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script> <script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.8074ae0006fba19803f5.js"></script> <script src="/~landscape/js/bundle/index.306e3d6fb700897c1bc1.js"></script>
</body> </body>
</html> </html>

View File

@ -25,8 +25,6 @@
^- (list @tas) ^- (list @tas)
:~ %group-store :~ %group-store
%metadata-store %metadata-store
%contact-store
%contact-hook
%invite-store %invite-store
%graph-store %graph-store
== ==

View File

@ -5,4 +5,4 @@ dojo:
it should return with the following hash: it should return with the following hash:
`0v7.v4dng.o33qi.kc497.5jc02.ke5es` `0v2.ifoe4.fbv35.aigir.66su4.fbspu`

View File

@ -103,6 +103,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
if ('addMembers' in json) { if ('addMembers' in json) {
const { resource, ships } = json.addMembers; const { resource, ships } = json.addMembers;
const resourcePath = resourceAsPath(resource); const resourcePath = resourceAsPath(resource);
if(!(resourcePath in state.groups)) {
return;
}
for (const member of ships) { for (const member of ships) {
state.groups[resourcePath].members.add(member); state.groups[resourcePath].members.add(member);
if ( if (

View File

@ -177,9 +177,6 @@ const ChatEditor = React.forwardRef<CodeMirrorShim, ChatEditorProps>(({ inCodeMo
}, [inCodeMode, placeholder]); }, [inCodeMode, placeholder]);
function messageChange(editor, data, value) { function messageChange(editor, data, value) {
if(value.endsWith('/')) {
editor.showHint(['test', 'foo']);
}
if (message !== '' && value == '') { if (message !== '' && value == '') {
setMessage(value); setMessage(value);
} }

View File

@ -138,7 +138,8 @@ class ChatWindow extends Component<
} }
if(this.unreadSet && if(this.unreadSet &&
this.dismissedInitialUnread() && this.dismissedInitialUnread() &&
this.virtualList!.startOffset() < 5) { this.virtualList!.startOffset() < 5 &&
document.hasFocus()) {
this.props.dismissUnread(); this.props.dismissUnread();
} }
} }

View File

@ -77,7 +77,7 @@ function GroupRoutes(props: { group: string; url: string }) {
return null; return null;
} }
if(!graphKeys.has(`${ship.slice(1)}/${name}`)) { if(!graphKeys.has(`${ship.slice(1)}/${name}`)) {
if(graphKeys.size > 0) { if(graphKeys.size > 1) { // TODO: Better loading logic see https://github.com/urbit/landscape/issues/1063
return <Redirect return <Redirect
to={toQuery( to={toQuery(
{ auto: 'y', redir: location.pathname }, { auto: 'y', redir: location.pathname },

View File

@ -2,7 +2,7 @@ import { BaseAnchor, Box, Center, Col, Icon, Row, Text } from '@tlon/indigo-reac
import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/api'; import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/api';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import { useHistory, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { import {
getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink
} from '~/logic/lib/permalinks'; } from '~/logic/lib/permalinks';
@ -76,7 +76,6 @@ function GraphPermalink(
} }
) { ) {
const { full = false, showOurContact, pending, graph, group, index, transcluded } = props; const { full = false, showOurContact, pending, graph, group, index, transcluded } = props;
const history = useHistory();
const location = useLocation(); const location = useLocation();
const { ship, name } = resourceFromPath(graph); const { ship, name } = resourceFromPath(graph);
const node = useGraphState( const node = useGraphState(
@ -114,11 +113,6 @@ function GraphPermalink(
const showTransclusion = Boolean(association && node && transcluded < 1); const showTransclusion = Boolean(association && node && transcluded < 1);
const permalink = getPermalinkForGraph(group, graph, index); const permalink = getPermalinkForGraph(group, graph, index);
const navigate = (e) => {
e.stopPropagation();
history.push(`/perma${permalink.slice(16)}`);
};
const [nodeGroupHost, nodeGroupName] = association?.group.split('/').slice(-2) ?? ['Unknown', 'Unknown']; const [nodeGroupHost, nodeGroupName] = association?.group.split('/').slice(-2) ?? ['Unknown', 'Unknown'];
const [nodeChannelHost, nodeChannelName] = association?.resource const [nodeChannelHost, nodeChannelName] = association?.resource
.split('/') .split('/')
@ -141,15 +135,16 @@ function GraphPermalink(
return ( return (
<Col <Col
as={Link}
to={`/perma${permalink.slice(16)}`}
width="100%" width="100%"
bg="white" bg="white"
maxWidth={full ? null : '500px'} maxWidth={full ? null : '500px'}
border={full ? null : '1'} border={full ? null : '1'}
borderColor="lightGray" borderColor="lightGray"
borderRadius={2} borderRadius={2}
cursor="pointer"
onClick={(e) => { onClick={(e) => {
navigate(e); e.stopPropagation();
}} }}
> >
{loading && association && !errored && Placeholder((association.metadata.config as GraphConfig).graph)} {loading && association && !errored && Placeholder((association.metadata.config as GraphConfig).graph)}
@ -167,7 +162,6 @@ function GraphPermalink(
showTransclusion={showTransclusion} showTransclusion={showTransclusion}
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)} icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
title={association.metadata.title} title={association.metadata.title}
permalink={permalink}
/> />
)} )}
{association && isInSameResource && transcluded === 2 && !loading && ( {association && isInSameResource && transcluded === 2 && !loading && (
@ -176,7 +170,6 @@ function GraphPermalink(
showTransclusion={showTransclusion} showTransclusion={showTransclusion}
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)} icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
title={association.metadata.title} title={association.metadata.title}
permalink={permalink}
/> />
)} )}
{isInSameResource && transcluded !== 2 && !loading && <Row height='2' />} {isInSameResource && transcluded !== 2 && !loading && <Row height='2' />}
@ -185,7 +178,6 @@ function GraphPermalink(
icon="Groups" icon="Groups"
showDetails={false} showDetails={false}
title={graph.slice(5)} title={graph.slice(5)}
permalink={permalink}
/> />
)} )}
</Col> </Col>
@ -195,7 +187,6 @@ function GraphPermalink(
function PermalinkDetails(props: { function PermalinkDetails(props: {
title: string; title: string;
icon: any; icon: any;
permalink: string;
showTransclusion?: boolean; showTransclusion?: boolean;
showDetails?: boolean; showDetails?: boolean;
known?: boolean; known?: boolean;

View File

@ -110,50 +110,52 @@ return false;
group={group} group={group}
isRelativeTime isRelativeTime
></Author> ></Author>
<Box opacity={hovering ? '100%' : '0%'}> {!post.pending &&
<Dropdown <Box opacity={hovering ? '100%' : '0%'}>
alignX="right" <Dropdown
alignY="top" alignX="right"
options={ alignY="top"
<Col options={
p="2" <Col
border="1" p="2"
borderRadius="1" border="1"
borderColor="lightGray" borderRadius="1"
backgroundColor="white" borderColor="lightGray"
gapY="2" backgroundColor="white"
> gapY="2"
<Action bg="white" onClick={doCopy}> >
{copyDisplay} <Action bg="white" onClick={doCopy}>
</Action> {copyDisplay}
{(window.ship == post?.author && !disabled) ? (
<ActionLink
color="blue"
to={{
pathname: props.baseUrl,
search: `?edit=${commentIndex}`
}}
>
Update
</ActionLink>
) : null}
{(window.ship == post?.author || ourRole == 'admin') &&
!disabled ? (
<Action
height="unset"
bg="white"
onClick={onDelete}
destructive
>
Delete
</Action> </Action>
) : null} {(window.ship == post?.author && !disabled) ? (
</Col> <ActionLink
} color="blue"
> to={{
<Icon icon="Ellipsis" /> pathname: props.baseUrl,
</Dropdown> search: `?edit=${commentIndex}`
</Box> }}
>
Update
</ActionLink>
) : null}
{(window.ship == post?.author || ourRole == 'admin') &&
!disabled ? (
<Action
height="unset"
bg="white"
onClick={onDelete}
destructive
>
Delete
</Action>
) : null}
</Col>
}
>
<Icon icon="Ellipsis" />
</Dropdown>
</Box>
}
</Row> </Row>
<GraphContent <GraphContent
borderRadius={1} borderRadius={1}

View File

@ -1,56 +1,63 @@
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react'; import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
import React, { ReactElement, useCallback } from 'react'; import React, { ReactElement, useCallback } from 'react';
import { useHistory } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useModal } from '~/logic/lib/useModal'; import { useModal } from '~/logic/lib/useModal';
import useMetadataState, { usePreview } from '~/logic/state/metadata'; import useMetadataState, { usePreview } from '~/logic/state/metadata';
import { PropFunc } from '~/types'; import { PropFunc } from '~/types';
import { JoinGroup } from '../landscape/components/JoinGroup'; import { JoinGroup } from '../landscape/components/JoinGroup';
import { MetadataIcon } from '../landscape/components/MetadataIcon'; import { MetadataIcon } from '../landscape/components/MetadataIcon';
export function GroupLink( type GroupLinkProps = {
props: { resource: string;
resource: string; detailed?: boolean;
detailed?: boolean; } & PropFunc<typeof Row>
} & PropFunc<typeof Row>
): ReactElement { export function GroupLink({
const { resource, ...rest } = props; resource,
borderColor,
...rest
}: GroupLinkProps): ReactElement {
const name = resource.slice(6); const name = resource.slice(6);
const joined = useMetadataState( const joined = useMetadataState(
useCallback(s => resource in s.associations.groups, [resource]) useCallback(s => resource in s.associations.groups, [resource])
); );
const history = useHistory();
const { modal, showModal } = useModal({ const { modal, showModal } = useModal({
modal: <JoinGroup autojoin={name} /> modal: <JoinGroup autojoin={name} />
}); });
const { preview } = usePreview(resource); const { preview } = usePreview(resource);
return ( return (
<Box <>
maxWidth="500px"
cursor='pointer'
{...rest}
onClick={(e) => {
e.stopPropagation();
}}
backgroundColor='white'
borderColor={props.borderColor}
>
{modal} {modal}
<Row <Row
width="100%" {...rest}
as={Link}
to={joined ? `/~landscape/ship/${name}` : `/perma/group/${name}`}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
e.stopPropagation();
if (e.metaKey || e.ctrlKey) {
return;
}
e.preventDefault();
showModal();
}}
flexShrink={1} flexShrink={1}
alignItems="center" alignItems="center"
width="100%"
maxWidth="500px"
py={2} py={2}
pr={2} pr={2}
onClick={ cursor='pointer'
joined ? () => history.push(`/~landscape/ship/${name}`) : showModal backgroundColor='white'
} borderColor={borderColor}
opacity={preview ? '1' : '0.6'} opacity={preview ? '1' : '0.6'}
> >
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} /> <MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
<Col> <Col>
<Text ml={2} fontWeight="medium" mono={!preview}> <Text ml={2} fontWeight="medium" mono={!preview}>
{preview ? preview.metadata.title : name} {preview ? preview.metadata.title : name}
</Text> </Text>
@ -70,6 +77,6 @@ export function GroupLink(
</Box> </Box>
</Col> </Col>
</Row> </Row>
</Box> </>
); );
} }

View File

@ -8,7 +8,7 @@ import {
Text Text
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util'; import { uxToHex } from '~/logic/lib/util';
import useContactState from '~/logic/state/contact'; import useContactState from '~/logic/state/contact';
@ -75,11 +75,12 @@ const StatusBar = (props) => {
> >
<Row> <Row>
<Button <Button
as={Link}
to="/"
width='32px' width='32px'
borderColor='lightGray' borderColor='lightGray'
mr={2} mr={2}
px={2} px={2}
onClick={() => history.push('/')}
{...props} {...props}
> >
<Icon icon='Dashboard' color='black' /> <Icon icon='Dashboard' color='black' />
@ -126,9 +127,10 @@ const StatusBar = (props) => {
<Icon icon="Bug" color="#000000" /> <Icon icon="Bug" color="#000000" />
</StatusBarItem> </StatusBarItem>
<StatusBarItem <StatusBarItem
as={Link}
to="/~landscape/messages"
width='32px' width='32px'
mr={2} mr={2}
onClick={() => props.history.push('/~landscape/messages')}
> >
<Icon icon='Messages' /> <Icon icon='Messages' />
</StatusBarItem> </StatusBarItem>
@ -150,24 +152,26 @@ const StatusBar = (props) => {
boxShadow='0px 0px 0px 3px' boxShadow='0px 0px 0px 3px'
> >
<Row <Row
as={Link}
to={`/~profile/~${ship}`}
color='black' color='black'
cursor='pointer' cursor='pointer'
fontSize={1} fontSize={1}
fontWeight='500' fontWeight='500'
px={3} px={3}
py={2} py={2}
onClick={() => history.push(`/~profile/~${ship}`)}
> >
View Profile View Profile
</Row> </Row>
<Row <Row
as={Link}
to="/~settings"
color='black' color='black'
cursor='pointer' cursor='pointer'
fontSize={1} fontSize={1}
fontWeight='500' fontWeight='500'
px={3} px={3}
py={2} py={2}
onClick={() => history.push('/~settings')}
> >
System Preferences System Preferences
</Row> </Row>

View File

@ -1,17 +1,20 @@
import { Col, ColProps } from '@tlon/indigo-react'; import { Col, ColProps } from '@tlon/indigo-react';
import { Post } from '@urbit/api'; import { Post } from '@urbit/api';
import React, { ReactElement } from 'react'; import React, { ReactElement, useCallback, useState } from 'react';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent'; import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
type TruncateProps = ColProps & { type TruncateProps = ColProps & {
truncate?: number; truncate: boolean;
} }
const TruncatedBox = styled(Col)<TruncateProps>` const TruncatedBox = styled(Col)<TruncateProps>`
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: ${p => p.truncate ?? 'unset'};
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
${p => p.truncate && css`
max-height: 300px;
mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 60%, transparent 100%);
`}
`; `;
interface PostContentProps { interface PostContentProps {
@ -20,16 +23,24 @@ interface PostContentProps {
isReply: boolean; isReply: boolean;
} }
const PostContent = (props: PostContentProps): ReactElement => { const PostContent = ({ post, isParent }: PostContentProps): ReactElement => {
const { post, isParent } = props; const [height, setHeight] = useState(0);
const showFade = !isParent && height >= 300;
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return ( return (
<TruncatedBox <TruncatedBox
ref={measuredRef}
truncate={showFade}
display="-webkit-box" display="-webkit-box"
width="90%" width="90%"
px={2} px={2}
pb={2} pb={2}
truncate={isParent ? null : 8}
textOverflow="ellipsis" textOverflow="ellipsis"
overflow="hidden" overflow="hidden"
> >

View File

@ -1,6 +1,6 @@
{ {
"name": "@urbit/api", "name": "@urbit/api",
"version": "1.2.0", "version": "1.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@urbit/api", "name": "@urbit/api",
"version": "1.2.0", "version": "1.3.0",
"description": "", "description": "",
"repository": { "repository": {
"type": "git", "type": "git",
@ -21,7 +21,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@urbit/eslint-config": "^1.0.1", "@urbit/eslint-config": "^1.0.2",
"big-integer": "^1.6.48", "big-integer": "^1.6.48",
"immer": "^9.0.1", "immer": "^9.0.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",

View File

@ -1,6 +1,6 @@
{ {
"name": "@urbit/eslint-config", "name": "@urbit/eslint-config",
"version": "1.0.1", "version": "1.0.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@urbit/eslint-config", "name": "@urbit/eslint-config",
"version": "1.0.1", "version": "1.0.2",
"description": "", "description": "",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "@urbit/http-api", "name": "@urbit/http-api",
"version": "1.2.3", "version": "1.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@urbit/http-api", "name": "@urbit/http-api",
"version": "1.2.3", "version": "1.3.0",
"license": "MIT", "license": "MIT",
"description": "Library to interact with an Urbit ship over HTTP", "description": "Library to interact with an Urbit ship over HTTP",
"repository": { "repository": {

View File

@ -164,14 +164,18 @@ export class Urbit {
/** /**
* Initializes the SSE pipe for the appropriate channel. * Initializes the SSE pipe for the appropriate channel.
*/ */
eventSource(): Promise<void> { async eventSource(): Promise<void> {
if(this.lastEventId === 0) { if(this.sseClientInitialized) {
// Can't receive events until the channel is open return Promise.resolve();
this.skipDebounce = true;
return this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' }).then(() => {});
} }
if(this.lastEventId === 0) {
// Can't receive events until the channel is open,
// so poke and open then
await this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' });
return;
}
this.sseClientInitialized = true;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.sseClientInitialized) {
const sseOptions: SSEOptions = { const sseOptions: SSEOptions = {
headers: {} headers: {}
}; };
@ -193,6 +197,7 @@ export class Urbit {
resolve(); resolve();
return; // everything's good return; // everything's good
} else { } else {
this.onError && this.onError(new Error('bad response'));
reject(); reject();
} }
}, },
@ -201,14 +206,13 @@ export class Urbit {
console.log('Received SSE: ', event); console.log('Received SSE: ', event);
} }
if (!event.id) return; if (!event.id) return;
this.ack(Number(event.id)); this.lastEventId = parseInt(event.id, 10);
if (event.data && JSON.parse(event.data)) { if((this.lastEventId - this.lastAcknowledgedEventId) > 20) {
this.ack(this.lastEventId);
const data: any = JSON.parse(event.data); }
if (data.response === 'diff') { if (event.data && JSON.parse(event.data)) {
this.clearQueue(); const data: any = JSON.parse(event.data);
}
if (data.response === 'poke' && this.outstandingPokes.has(data.id)) { if (data.response === 'poke' && this.outstandingPokes.has(data.id)) {
const funcs = this.outstandingPokes.get(data.id); const funcs = this.outstandingPokes.get(data.id);
@ -221,8 +225,8 @@ export class Urbit {
console.error('Invalid poke response', data); console.error('Invalid poke response', data);
} }
this.outstandingPokes.delete(data.id); this.outstandingPokes.delete(data.id);
} else if (data.response === 'subscribe' || } else if (data.response === 'subscribe'
(data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) { && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id); const funcs = this.outstandingSubscriptions.get(data.id);
if (data.hasOwnProperty('err')) { if (data.hasOwnProperty('err')) {
console.error(data.err); console.error(data.err);
@ -231,7 +235,11 @@ export class Urbit {
} }
} else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) { } else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id); const funcs = this.outstandingSubscriptions.get(data.id);
funcs.event(data.json); try {
funcs.event(data.json);
} catch (e) {
console.error('Failed to call subscription event callback', e);
}
} else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) { } else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id); const funcs = this.outstandingSubscriptions.get(data.id);
funcs.quit(data); funcs.quit(data);
@ -257,10 +265,7 @@ export class Urbit {
}, },
}); });
this.sseClientInitialized = true; })
}
resolve();
});
} }
/** /**
@ -272,12 +277,6 @@ export class Urbit {
this.abort.abort(); this.abort.abort();
this.abort = new AbortController(); this.abort = new AbortController();
this.uid = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`; this.uid = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`;
if(this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.calm = true;
this.outstandingJSON = [];
this.lastEventId = 0; this.lastEventId = 0;
this.lastAcknowledgedEventId = 0; this.lastAcknowledgedEventId = 0;
this.outstandingSubscriptions = new Map(); this.outstandingSubscriptions = new Map();
@ -299,6 +298,7 @@ export class Urbit {
* @param eventId The event to acknowledge. * @param eventId The event to acknowledge.
*/ */
private async ack(eventId: number): Promise<number | void> { private async ack(eventId: number): Promise<number | void> {
this.lastAcknowledgedEventId = eventId;
const message: Message = { const message: Message = {
action: 'ack', action: 'ack',
'event-id': eventId 'event-id': eventId
@ -307,90 +307,18 @@ export class Urbit {
return eventId; return eventId;
} }
/** private async sendJSONtoChannel(...json: Message[]): Promise<void> {
* This is a wrapper method that can be used to send any action with data. const response = await fetch(this.channelUrl, {
* ...this.fetchOptions,
* Every message sent has some common parameters, like method, headers, and data method: 'PUT',
* structure, so this method exists to prevent duplication. body: JSON.stringify(json)
*
* @param action The action to send
* @param data The data to send with the action
*
* @returns void | number If successful, returns the number of the message that was sent
*/
// async sendMessage(action: Action, data?: object): Promise<number | void> {
// const id = this.getEventId();
// if (this.verbose) {
// console.log(`Sending message ${id}:`, action, data,);
// }
// const message: Message = { id, action, ...data };
// await this.sendJSONtoChannel(message);
// return id;
// }
private outstandingJSON: Message[] = [];
private debounceTimer: NodeJS.Timeout = null;
private debounceInterval = 10;
private skipDebounce = false;
private calm = true;
private sendJSONtoChannel(json: Message): Promise<boolean | void> {
this.outstandingJSON.push(json);
return this.processQueue();
}
private processQueue(): Promise<boolean | void> {
return new Promise(async (resolve, reject) => {
const process = async () => {
if (this.calm) {
if (this.outstandingJSON.length === 0) resolve(true);
this.calm = false; // We are now occupied
const json = this.outstandingJSON;
const body = JSON.stringify(json);
this.outstandingJSON = [];
if (body === '[]') {
this.calm = true;
return resolve(false);
}
try {
const response = await fetch(this.channelUrl, {
...this.fetchOptions,
method: 'PUT',
body
});
if(!response.ok) {
throw new Error('failed to PUT');
}
} catch (error) {
console.log(error);
json.forEach(failed => this.outstandingJSON.push(failed));
if (this.onError) {
this.onError(error);
} else {
throw error;
}
}
this.calm = true;
if (!this.sseClientInitialized) {
this.eventSource().then(resolve); // We can open the channel for subscriptions once we've sent data over it
}
resolve(true);
} else {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(process, this.debounceInterval);
resolve(false);
}
}
if(this.skipDebounce) {
process();
this.skipDebounce = false;
}
this.debounceTimer = setTimeout(process, this.debounceInterval);
}); });
if(!response.ok) {
throw new Error('Failed to PUT channel');
}
if(!this.sseClientInitialized) {
await this.eventSource();
}
} }
/** /**
@ -433,24 +361,6 @@ export class Urbit {
}); });
} }
// resetDebounceTimer() {
// if (this.debounceTimer) {
// clearTimeout(this.debounceTimer);
// this.debounceTimer = null;
// }
// this.calm = false;
// this.debounceTimer = setTimeout(() => {
// this.calm = true;
// }, this.debounceInterval);
// }
clearQueue() {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
/** /**
* Pokes a ship with data. * Pokes a ship with data.
* *
@ -458,7 +368,7 @@ export class Urbit {
* @param mark The mark of the data being sent * @param mark The mark of the data being sent
* @param json The data to send * @param json The data to send
*/ */
poke<T>(params: PokeInterface<T>): Promise<number> { async poke<T>(params: PokeInterface<T>): Promise<number> {
const { const {
app, app,
mark, mark,
@ -472,29 +382,30 @@ export class Urbit {
ship: this.ship, ship: this.ship,
...params ...params
}; };
return new Promise((resolve, reject) => { const message: Message = {
const message: Message = { id: this.getEventId(),
id: this.getEventId(), action: 'poke',
action: 'poke', ship,
ship, app,
app, mark,
mark, json
json };
}; const [send, result] = await Promise.all([
this.outstandingPokes.set(message.id, { this.sendJSONtoChannel(message),
onSuccess: () => { new Promise<number>((resolve, reject) => {
onSuccess(); this.outstandingPokes.set(message.id, {
resolve(message.id); onSuccess: () => {
}, onSuccess();
onError: (event) => { resolve(message.id);
onError(event); },
reject(event.err); onError: (event) => {
} onError(event);
}); reject(event.err);
this.sendJSONtoChannel(message).then(() => { }
resolve(message.id); });
}); })
}); ]);
return result;
} }
/** /**

View File

@ -1,5 +1,7 @@
import Urbit from '../src'; import Urbit from '../src';
import { Readable } from 'streams'; import { Readable } from 'streams';
import 'jest';
function fakeSSE(messages = [], timeout = 0) { function fakeSSE(messages = [], timeout = 0) {
const ourMessages = [...messages]; const ourMessages = [...messages];
@ -60,7 +62,6 @@ describe('Initialisation', () => {
let fetchSpy; let fetchSpy;
beforeEach(() => { beforeEach(() => {
airlock = new Urbit('', '+code'); airlock = new Urbit('', '+code');
airlock.debounceInterval = 10;
}); });
afterEach(() => { afterEach(() => {
fetchSpy.mockReset(); fetchSpy.mockReset();
@ -73,7 +74,7 @@ describe('Initialisation', () => {
Promise.resolve({ ok: true, body: fakeSSE() }) Promise.resolve({ ok: true, body: fakeSSE() })
) )
.mockImplementationOnce(() => .mockImplementationOnce(() =>
Promise.resolve({ ok: true, body: fakeSSE() }) Promise.resolve({ ok: true, body: fakeSSE([ack(1)]) })
); );
await airlock.eventSource(); await airlock.eventSource();
@ -82,13 +83,16 @@ describe('Initialisation', () => {
it('should handle failures', async () => { it('should handle failures', async () => {
fetchSpy = jest.spyOn(window, 'fetch'); fetchSpy = jest.spyOn(window, 'fetch');
fetchSpy fetchSpy
.mockImplementation(() => .mockImplementationOnce(() =>
Promise.resolve({ ok: true, body: fakeSSE() })
)
.mockImplementationOnce(() =>
Promise.resolve({ ok: false, body: fakeSSE() }) Promise.resolve({ ok: false, body: fakeSSE() })
) )
airlock.onError = jest.fn(); airlock.onError = jest.fn();
try { try {
await airlock.eventSource(); await airlock.eventSource();
wait(100);
} catch (e) { } catch (e) {
expect(airlock.onError).toHaveBeenCalled(); expect(airlock.onError).toHaveBeenCalled();
} }