├── .prettierrc.js ├── README.md ├── SUMMARY.md ├── desk ├── app │ └── tome.hoon ├── desk.bill ├── desk.docket-0 ├── desk.ship ├── lib │ ├── dbug.hoon │ ├── default-agent.hoon │ ├── docket.hoon │ ├── mip.hoon │ ├── realm-lib.hoon │ ├── skeleton.hoon │ ├── spaces.hoon │ ├── strand.hoon │ ├── strandio.hoon │ ├── tomelib.hoon │ └── verb.hoon ├── mar │ ├── bill.hoon │ ├── docket-0.hoon │ ├── feed │ │ ├── action.hoon │ │ └── update.hoon │ ├── hoon.hoon │ ├── json.hoon │ ├── kelvin.hoon │ ├── kv │ │ ├── action.hoon │ │ └── update.hoon │ ├── mime.hoon │ ├── noun.hoon │ ├── ship.hoon │ ├── tome │ │ └── action.hoon │ └── txt.hoon ├── sur │ ├── docket.hoon │ ├── membership.hoon │ ├── rooms-v2.hoon │ ├── spaces │ │ ├── path.hoon │ │ └── store.hoon │ ├── spider.hoon │ ├── station.hoon │ ├── tome.hoon │ ├── verb.hoon │ └── visas.hoon ├── sys.kelvin └── ted │ ├── feed-poke-tunnel.hoon │ └── kv-poke-tunnel.hoon ├── pkg ├── .gitignore ├── README.md ├── jest.config.ts ├── package-lock.json ├── package.json ├── src │ ├── classes │ │ ├── Tome.ts │ │ ├── constants.ts │ │ └── index.ts │ ├── index.ts │ └── types.ts ├── tests │ └── Tome.test.ts └── tsconfig.json ├── quick-start.md └── reference ├── feed-+-log-api.md ├── initializing-stores.md ├── key-value-api.md └── managing-permissions.md /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 4, 4 | semi: false, 5 | singleQuote: true, 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Urbit's web3 and composability bridge 3 | --- 4 | 5 | # TomeDB 6 | 7 | TomeDB is an **Urbit database and JavaScript client package** with native permissioning and subscription support. With TomeDB, developers can build full-stack Urbit applications entirely in JavaScript **(no Hoon)**. Tome currently supports multiple storage types, including key-value, log, and feed. 8 | 9 | ## **Features** 10 | 11 | * _Designed for migration from preexisting dApps_: With the key-value store, TomeDB can dynamically use JavaScript local storage if an Urbit connection is not made. This means that developers can prepare and **distribute applications both on and off Urbit from a single codebase**. 12 | * _Enables app composability_: With Urbit, your users' data is theirs, forever. Applications using TomeDB can directly read and write from other data stores, with the simplicity of saving files to disk. 13 | * _Preload, caching, and callback system_: TomeDB supports both preloading and caching values directly in JavaScript, reducing the number of requests to Urbit and making it easy to deliver a snappy user experience. Callback functions are also provided to make re-rendering and state management a breeze. 14 | * _Fully integrated and modular permissions_: Developers can specify exactly which users or groups have read / write / overwrite access for each data store. 15 | * _Bootstraps Automatically_: Applications using TomeDB will create and set access to data stores on launch - no user or developer configuration required. 16 | 17 | ## Example 18 | 19 | Here’s a simple example of setting and retrieving a value with Tome and an Urbit connection: 20 | 21 | ```tsx 22 | import Urbit from '@urbit/http-api' 23 | import Tome from '@holium/tome-db' 24 | 25 | const api = new Urbit('', '', window.desk) 26 | api.ship = window.ship 27 | 28 | const db = await Tome.init(api) 29 | const store = await db.keyvalue() 30 | 31 | const result = await store.set('foo', 'bar') 32 | // result === true 33 | 34 | const value = await store.get('foo') 35 | // value === 'bar' 36 | ``` 37 | 38 | ## Limitations 39 | 40 | * TomeDB currently cannot support large datasets or concurrent user counts. All data is stored and delivered by the host ship, whose max capacity is \~2-8 GB. Future plans include support for load balancing and data distribution. 41 | * Data stores are somewhat static (key-value, log, or feed). TomeDB is not a full graph or relational database replacement. It can still be useful for rapid prototyping, however. 42 | 43 | ## Quick Links 44 | 45 | * [Documentation](https://docs.holium.com/tomedb/) 46 | * [Quick start / how to install](https://docs.holium.com/tomedb/quick-start/) 47 | * [Example application](https://github.com/ajlamarc/racket) 48 | * [TomeDB + Replit demo](https://replit.com/@ajlamarc/Urbit-on-Replit?v=1) 49 | * [Source code](https://github.com/holium/tome-db) 50 | * [Report bugs or request new features](https://github.com/holium/tome-db/issues) 51 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [TomeDB](README.md) 4 | * [Quick Start](quick-start.md) 5 | 6 | ## Reference 7 | 8 | * [Initializing Stores](reference/initializing-stores.md) 9 | * [Managing Permissions](reference/managing-permissions.md) 10 | * [Key-value API](reference/key-value-api.md) 11 | * [Feed + Log API](reference/feed-+-log-api.md) 12 | -------------------------------------------------------------------------------- /desk/app/tome.hoon: -------------------------------------------------------------------------------- 1 | :: Tome DB - a Holium collaboration 2 | :: by ~larryx-woldyr 3 | :: 4 | /- *tome, s-p=spaces-path 5 | /+ r-l=realm-lib 6 | /+ verb, dbug, defa=default-agent 7 | /+ *mip 8 | :: 9 | |% 10 | :: 11 | +$ versioned-state $%(state-0) 12 | :: 13 | +$ state-0 [%0 tome=(mip path:s-p app tome-data) subs=(set path)] :: subs is data paths we are subscribed to 14 | :: 15 | +$ card card:agent:gall 16 | -- 17 | :: 18 | %+ verb & 19 | %- agent:dbug 20 | =| state-0 21 | =* state - 22 | :: 23 | ^- agent:gall 24 | :: 25 | =< 26 | |_ =bowl:gall 27 | +* this . 28 | def ~(. (defa this %|) bowl) 29 | eng ~(. +> [bowl ~]) 30 | ++ on-init 31 | ^- (quip card _this) 32 | ~> %bout.[0 '%tome +on-init'] 33 | =^ cards state 34 | abet:init:eng 35 | [cards this] 36 | :: 37 | ++ on-save 38 | ^- vase 39 | ~> %bout.[0 '%tome +on-save'] 40 | !>(state) 41 | :: 42 | ++ on-load 43 | |= ole=vase 44 | ~> %bout.[0 '%tome +on-load'] 45 | ^- (quip card _this) 46 | =^ cards state 47 | abet:(load:eng ole) 48 | [cards this] 49 | :: 50 | ++ on-poke 51 | |= cag=cage 52 | ~> %bout.[0 '%tome +on-poke'] 53 | ^- (quip card _this) 54 | =^ cards state abet:(poke:eng cag) 55 | [cards this] 56 | :: 57 | ++ on-peek 58 | |= =path 59 | ~> %bout.[0 '%tome +on-peek'] 60 | ^- (unit (unit cage)) 61 | (peek:eng path) 62 | :: 63 | ++ on-agent 64 | |= [pol=(pole knot) sig=sign:agent:gall] 65 | ~> %bout.[0 '%tome +on-agent'] 66 | ^- (quip card _this) 67 | =^ cards state abet:(dude:eng pol sig) 68 | [cards this] 69 | :: 70 | ++ on-arvo 71 | |= [wir=wire sig=sign-arvo] 72 | ~> %bout.[0 '%tome +on-arvo'] 73 | ^- (quip card _this) 74 | `this 75 | :: 76 | ++ on-watch 77 | |= =path 78 | ~> %bout.[0 '%tome +on-watch'] 79 | ^- (quip card _this) 80 | =^ cards state abet:(watch:eng path) 81 | [cards this] 82 | :: 83 | ++ on-fail 84 | ~> %bout.[0 '%tome +on-fail'] 85 | on-fail:def 86 | :: 87 | ++ on-leave 88 | ~> %bout.[0 '%tome +on-leave'] 89 | on-leave:def 90 | -- 91 | |_ [bol=bowl:gall dek=(list card)] 92 | +* dat . 93 | ++ emit |=(=card dat(dek [card dek])) 94 | ++ emil |=(lac=(list card) dat(dek (welp lac dek))) 95 | ++ abet 96 | ^- (quip card _state) 97 | [(flop dek) state] 98 | :: 99 | ++ init 100 | ^+ dat 101 | dat 102 | :: 103 | ++ load 104 | |= vaz=vase 105 | ^+ dat 106 | ?> ?=([%0 *] q.vaz) 107 | dat(state !<(state-0 vaz)) 108 | :: +watch: handle on-watch 109 | :: 110 | ++ watch 111 | |= pol=(pole knot) 112 | ^+ dat 113 | ?+ pol ~|(bad-watch-path/pol !!) 114 | [%kv ship=@ space=@ app=@ bucket=@ rest=*] 115 | =/ ship `@p`(slav %p ship.pol) 116 | =/ space (woad space.pol) 117 | =^ cards state 118 | kv-abet:(kv-watch:(kv-abed:kv [ship space app.pol bucket.pol]) rest.pol) 119 | (emil cards) 120 | :: 121 | [%feed ship=@ space=@ app=@ bucket=@ log=@ rest=*] 122 | =/ ship `@p`(slav %p ship.pol) 123 | =/ space (woad space.pol) 124 | =/ log=? =(log.pol 'log') 125 | =^ cards state 126 | fe-abet:(fe-watch:(fe-abed:fe [ship space app.pol bucket.pol log]) rest.pol) 127 | (emil cards) 128 | == 129 | :: +dude: handle on-agent 130 | :: 131 | ++ dude 132 | |= [pol=(pole knot) sig=sign:agent:gall] 133 | ^+ dat 134 | =^ cards state 135 | ?+ pol ~|(bad-dude-wire/pol !!) 136 | [%kv ship=@ space=@ app=@ bucket=@ rest=*] 137 | :: 138 | =/ ship `@p`(slav %p ship.pol) 139 | ?+ -.sig `state 140 | %fact kv-abet:(kv-dude:(kv-abed:kv [ship space.pol app.pol bucket.pol]) cage.sig) 141 | :: 142 | %kick 143 | =. subs (~(del in subs) pol) 144 | kv-abet:(kv-view:(kv-abed:kv [ship space.pol app.pol bucket.pol]) rest.pol) 145 | :: 146 | %watch-ack 147 | ?+ rest.pol ~|(bad-kv-watch-ack-path/rest.pol !!) 148 | [%data %all ~] 149 | ?~ p.sig 150 | :: sub success, store that we're subscribed 151 | =. subs (~(put in subs) pol) 152 | `state 153 | ((slog leaf/"kv-watch nack" ~) `state) 154 | :: 155 | [%perm ~] 156 | %. `state 157 | ?~(p.sig same (slog leaf/"kv-watch nack" ~)) 158 | :: 159 | == 160 | :: 161 | == 162 | [%feed ship=@ space=@ app=@ bucket=@ log=@ rest=*] 163 | :: 164 | =/ ship `@p`(slav %p ship.pol) 165 | =/ log=? =(log.pol 'log') 166 | ?+ -.sig `state 167 | %fact fe-abet:(fe-dude:(fe-abed:fe [ship space.pol app.pol bucket.pol log]) cage.sig) 168 | :: 169 | %kick 170 | =. subs (~(del in subs) pol) 171 | fe-abet:(fe-view:(fe-abed:fe [ship space.pol app.pol bucket.pol log]) rest.pol) 172 | :: 173 | %watch-ack 174 | ?+ rest.pol ~|(bad-feed-watch-ack-path/rest.pol !!) 175 | [%data %all ~] 176 | ?~ p.sig 177 | =. subs (~(put in subs) pol) 178 | `state 179 | ((slog leaf/"feed-watch nack" ~) `state) 180 | :: 181 | [%perm ~] 182 | %. `state 183 | ?~(p.sig same (slog leaf/"feed-watch nack" ~)) 184 | :: 185 | == 186 | == 187 | :: 188 | == 189 | (emil cards) 190 | :: +poke: handle on-poke 191 | :: 192 | ++ poke 193 | |= [mar=mark vaz=vase] 194 | ^+ dat 195 | =^ cards state 196 | ?+ mar ~|(bad-tome-mark/mar !!) 197 | %tome-action 198 | =/ act !<(tome-action vaz) 199 | =/ ship `@p`(slav %p ship.act) 200 | ?- -.act 201 | %init-tome 202 | ?. =(our.bol src.bol) ~|('no-foreign-init-tome' !!) 203 | ?: (~(has bi tome) [ship space.act] app.act) 204 | `state 205 | `state(tome (~(put bi tome) [ship space.act] app.act *tome-data)) 206 | :: 207 | :: the following init pokes expect an %init-tome to already have been done. 208 | %init-kv 209 | ?. =(our.bol src.bol) ~|('no-foreign-init-kv' !!) 210 | =+ tod=(~(got bi tome) [ship space.act] app.act) 211 | ?: (~(has by store.tod) bucket.act) 212 | `state 213 | =. store.tod (~(put by store.tod) bucket.act [perm.act *invited *kv-meta *kv-data]) 214 | `state(tome (~(put bi tome) [ship space.act] app.act tod)) 215 | :: 216 | %init-feed 217 | ?. =(our.bol src.bol) ~|('no-foreign-init-feed' !!) 218 | =+ tod=(~(got bi tome) [ship space.act] app.act) 219 | ?: (~(has by feed.tod) [bucket.act log.act]) 220 | `state 221 | =. feed.tod (~(put by feed.tod) [bucket.act log.act] [perm.act *feed-ids *invited *feed-data]) 222 | `state(tome (~(put bi tome) [ship space.act] app.act tod)) 223 | :: 224 | == 225 | :: 226 | %kv-action 227 | =/ act !<(kv-action vaz) 228 | =/ ship `@p`(slav %p ship.act) 229 | =* do kv-abet:(kv-poke:(kv-abed:kv [ship space.act app.act bucket.act]) act) 230 | ?- -.act 231 | %set-value do 232 | %remove-value do 233 | %clear-kv do 234 | %verify-kv do 235 | %perm-kv 236 | ?. =(our.bol ship) ~|('no-perm-foreign-kv' !!) 237 | do 238 | %invite-kv 239 | ?. =(our.bol ship) ~|('no-invite-foreign-kv' !!) 240 | do 241 | %team-kv 242 | ?: =(our.bol ship) ~|('no-team-local-kv' !!) 243 | kv-abet:(kv-view:(kv-abed:kv [ship space.act app.act bucket.act]) [%perm ~]) 244 | %watch-kv 245 | ?: =(our.bol ship) ~|('no-watch-local-kv' !!) 246 | kv-abet:(kv-view:(kv-abed:kv [ship space.act app.act bucket.act]) [%data %all ~]) 247 | == 248 | :: 249 | %feed-action 250 | =/ act !<(feed-action vaz) 251 | =/ ship `@p`(slav %p ship.act) 252 | =* do fe-abet:(fe-poke:(fe-abed:fe [ship space.act app.act bucket.act log.act]) act) 253 | ?- -.act 254 | %new-post do 255 | %delete-post do 256 | %edit-post do 257 | %clear-feed do 258 | %verify-feed do 259 | %set-post-link do 260 | %remove-post-link do 261 | %perm-feed 262 | ?. =(our.bol ship) ~|('no-perm-foreign-feed' !!) 263 | do 264 | %invite-feed 265 | ?. =(our.bol ship) ~|('no-invite-foreign-feed' !!) 266 | do 267 | %team-feed 268 | ?: =(our.bol ship) ~|('no-team-local-feed' !!) 269 | fe-abet:(fe-view:(fe-abed:fe [ship space.act app.act bucket.act log.act]) [%perm ~]) 270 | %watch-feed 271 | ?: =(our.bol ship) ~|('no-watch-local-feed' !!) 272 | fe-abet:(fe-view:(fe-abed:fe [ship space.act app.act bucket.act log.act]) [%data %all ~]) 273 | == 274 | == 275 | (emil cards) 276 | :: +peek: handle on-peek 277 | :: 278 | ++ peek 279 | |= pol=(pole knot) 280 | ^- (unit (unit cage)) 281 | ?+ pol ~|(bad-tome-peek-path/pol !!) 282 | [%x %kv ship=@ space=@ app=@ bucket=@ rest=*] 283 | =/ ship `@p`(slav %p ship.pol) 284 | =/ space (woad space.pol) 285 | (kv-peek:(kv-abed:kv [ship space app.pol bucket.pol]) rest.pol) 286 | :: 287 | [%x %feed ship=@ space=@ app=@ bucket=@ log=@ rest=*] 288 | =/ ship `@p`(slav %p ship.pol) 289 | =/ space (woad space.pol) 290 | =/ log=? =(log.pol 'log') 291 | (fe-peek:(fe-abed:fe [ship space app.pol bucket.pol log]) rest.pol) 292 | :: 293 | == 294 | :: 295 | :: +kv: keyvalue engine 296 | :: 297 | ++ kv 298 | |_ $: shi=ship 299 | spa=space 300 | =app 301 | buc=bucket 302 | tod=tome-data 303 | per=perm 304 | inv=invited 305 | meta=kv-meta 306 | data=kv-data 307 | caz=(list card) 308 | data-pax=path 309 | perm-pax=path 310 | == 311 | +* kv . 312 | ++ kv-emit |=(c=card kv(caz [c caz])) 313 | ++ kv-emil |=(lc=(list card) kv(caz (welp lc caz))) 314 | ++ kv-abet 315 | ^- (quip card _state) 316 | =. store.tod (~(put by store.tod) buc [per inv meta data]) 317 | [(flop caz) state(tome (~(put bi tome) [shi spa] app tod))] 318 | :: +kv-abed: initialize nested core. only works when the map entries already exist 319 | :: 320 | ++ kv-abed 321 | |= [p=ship s=space a=^app b=bucket] 322 | =/ tod (~(got bi tome) [p s] a) 323 | =/ sto (~(got by store.tod) b) 324 | =/ pp `@tas`(scot %p p) 325 | =/ suv (wood s) 326 | %= kv 327 | shi p 328 | spa s 329 | app a 330 | buc b 331 | tod tod 332 | per perm.sto 333 | inv invites.sto 334 | meta meta.sto 335 | data data.sto 336 | data-pax /kv/[pp]/[suv]/[a]/[b]/data/all 337 | perm-pax /kv/[pp]/[suv]/[a]/[b]/perm 338 | == 339 | :: +kv-dude: handle foreign kv updates (facts) 340 | :: 341 | ++ kv-dude 342 | |= cag=cage 343 | ^+ kv 344 | ?< =(our.bol shi) 345 | ?+ p.cag ~|('bad-kv-dude' !!) 346 | %kv-update 347 | =/ upd !<(kv-update q.cag) 348 | ?+ -.upd ~|('bad-kv-update' !!) 349 | %set 350 | %= kv 351 | data (~(put by data) key.upd s+value.upd) 352 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz] 353 | == 354 | :: 355 | %remove 356 | %= kv 357 | data (~(del by data) key.upd) 358 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz] 359 | == 360 | :: 361 | %clear 362 | %= kv 363 | data *kv-data 364 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz] 365 | == 366 | :: 367 | %all 368 | %= kv 369 | data data.upd 370 | caz [[%give %fact ~[data-pax] %kv-update !>(upd)] caz] 371 | == 372 | :: 373 | %perm 374 | =/ lc :~ [%give %fact ~[perm-pax] %kv-update !>(upd)] 375 | [%pass perm-pax %agent [shi %tome] %leave ~] 376 | == 377 | %= kv 378 | per [read=%yes +.upd] 379 | caz (welp lc caz) 380 | == 381 | :: 382 | == 383 | == 384 | :: +kv-watch: handle incoming kv watch requests 385 | :: 386 | ++ kv-watch 387 | |= rest=(pole knot) 388 | ^+ kv 389 | ?> (kv-perm %read) 390 | ?+ rest ~|(bad-kv-watch-path/rest !!) 391 | [%perm ~] 392 | %- kv-emit 393 | [%give %fact ~ %kv-update !>(`kv-update`kv-team)] 394 | :: 395 | [%data %all ~] 396 | %- kv-emit 397 | [%give %fact ~ %kv-update !>(`kv-update`[%all data])] 398 | :: 399 | == 400 | :: +kv-poke: handle kv poke requests 401 | :: cm = current metadata 402 | :: nm = new metadata 403 | :: 404 | ++ kv-poke 405 | |= act=kv-action 406 | ^+ kv 407 | :: right now live updates only go to the subscribeAll endpoint 408 | ?+ -.act ~|('bad-kv-action' !!) 409 | %set-value 410 | =+ cm=(~(gut by meta) key.act ~) 411 | =* lvl 412 | ?~ cm 413 | %create 414 | ?:(=(src.bol created-by.cm) %create %overwrite) 415 | ?> (kv-perm lvl) 416 | :: equivalent value is already set, do nothing. 417 | ?: =(s+value.act (~(gut by data) key.act ~)) kv 418 | :: 419 | =/ nm 420 | ?~ cm 421 | :: this value is new, so create new metadata entry alongside it 422 | [src.bol src.bol now.bol now.bol] 423 | :: this value already exists, so update its metadata 424 | [created-by.cm src.bol created-at.cm now.bol] 425 | :: 426 | %= kv 427 | meta (~(put by meta) key.act nm) 428 | data (~(put by data) key.act s+value.act) 429 | caz [[%give %fact ~[data-pax] %kv-update !>(`kv-update`[%set key.act value.act])] caz] 430 | == 431 | :: 432 | %remove-value 433 | =+ cm=(~(gut by meta) key.act ~) 434 | =* lvl 435 | ?~ cm 436 | %create 437 | ?:(=(src.bol created-by.cm) %create %overwrite) 438 | ?> (kv-perm lvl) 439 | :: value doesn't exist, do nothing 440 | ?~ cm 441 | kv 442 | :: 443 | %= kv 444 | meta (~(del by meta) key.act) 445 | data (~(del by data) key.act) 446 | caz [[%give %fact ~[data-pax] %kv-update !>(`kv-update`[%remove key.act])] caz] 447 | == 448 | :: 449 | %clear-kv 450 | :: could check if all values are ours for %create perm level, but that's overkill 451 | ?> (kv-perm %overwrite) 452 | ?~ meta kv :: nothing to clear 453 | %= kv 454 | meta *kv-meta 455 | data *kv-data 456 | caz [[%give %fact ~[data-pax] %kv-update !>(`kv-update`[%clear ~])] caz] 457 | == 458 | :: 459 | %verify-kv 460 | :: The bucket must exist to get this far, so we just need to verify read permissions. 461 | ?> (kv-perm %read) 462 | kv 463 | :: 464 | %perm-kv 465 | :: force everyone to re-subscribe 466 | kv(per perm.act, caz [[%give %kick ~[data-pax] ~] caz]) 467 | :: 468 | %invite-kv 469 | =/ guy `@p`(slav %p guy.act) 470 | %= kv 471 | inv (~(put by inv) guy level.act) 472 | caz [[%give %kick ~[data-pax] `guy] caz] 473 | == 474 | :: 475 | == 476 | :: +kv-peek: handle kv peek requests 477 | :: 478 | ++ kv-peek 479 | |= rest=(pole knot) 480 | ^- (unit (unit cage)) 481 | :: no perms check since no remote scry 482 | ?+ rest ~|(bad-kv-peek-path/rest !!) 483 | [%data %all ~] 484 | ``kv-update+!>(`kv-update`[%all data]) 485 | :: 486 | [%data %key key=@t ~] 487 | ``kv-update+!>(`kv-update`[%get (~(gut by data) key.rest ~)]) 488 | :: 489 | == 490 | :: +kv-view: start watching foreign kv (permissions or path) 491 | :: 492 | ++ kv-view 493 | |= rest=(pole knot) 494 | ^+ kv 495 | ?+ rest ~|(bad-kv-watch-path/rest !!) 496 | [%perm ~] 497 | (kv-emit [%pass perm-pax %agent [shi %tome] %watch perm-pax]) 498 | :: 499 | [%data %all ~] 500 | ?: (~(has in subs) data-pax) kv 501 | (kv-emit [%pass data-pax %agent [shi %tome] %watch data-pax]) 502 | :: 503 | == 504 | :: +kv-perm: check a permission level, return true if allowed 505 | :: 506 | ++ kv-perm 507 | |= [lvl=?(%read %create %overwrite)] 508 | ^- ? 509 | ?: =(src.bol our.bol) %.y :: always allow local 510 | =/ bro (~(gut by inv) src.bol ~) 511 | ?- lvl 512 | %read 513 | ?~ bro 514 | ?- read.per 515 | %unset %.n 516 | %no %.n 517 | %our %.n :: it's not us, so no. 518 | %open %.y 519 | %yes %.y 520 | %space 521 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun) 522 | ?> ?=(%is-member -.memb) 523 | is-member.memb 524 | == 525 | :: use invite level to determine 526 | ?:(?=(%block bro) %.n %.y) 527 | :: 528 | %create 529 | ?~ bro 530 | ?- write.per 531 | %unset %.n 532 | %no %.n 533 | %our %.n 534 | %open %.y 535 | %yes %.y 536 | %space 537 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun) 538 | ?> ?=(%is-member -.memb) 539 | is-member.memb 540 | == 541 | ?:(?=(?(%block %read) bro) %.n %.y) 542 | :: 543 | %overwrite 544 | ?~ bro 545 | ?- admin.per 546 | %unset %.n 547 | %no %.n 548 | %our %.n 549 | %open %.y 550 | %yes %.y 551 | %space 552 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun) 553 | ?> ?=(%is-member -.memb) 554 | is-member.memb 555 | == 556 | ?:(?=(?(%block %read %write) bro) %.n %.y) 557 | :: 558 | == 559 | :: +kv-team: get write/admin permissions for a ship 560 | :: 561 | ++ kv-team 562 | =/ write ?:((kv-perm %create) %yes %no) 563 | =/ admin ?:((kv-perm %overwrite) %yes %no) 564 | [%perm write admin] 565 | :: 566 | -- 567 | :: 568 | :: +fe: feed engine 569 | :: 570 | ++ fe 571 | |_ $: shi=ship 572 | spa=space 573 | ap=app 574 | buc=bucket 575 | lo=log 576 | tod=tome-data 577 | per=perm 578 | ids=feed-ids 579 | inv=invited 580 | data=feed-data 581 | caz=(list card) 582 | data-pax=path 583 | perm-pax=path 584 | == 585 | +* fe . 586 | ++ fe-emit |=(c=card fe(caz [c caz])) 587 | ++ fe-emil |=(lc=(list card) fe(caz (welp lc caz))) 588 | ++ fe-abet 589 | ^- (quip card _state) 590 | =. feed.tod (~(put by feed.tod) [buc lo] [per ids inv data]) 591 | [(flop caz) state(tome (~(put bi tome) [shi spa] ap tod))] 592 | :: +kv-abed: initialize nested core. only works when the map entries already exist 593 | :: 594 | ++ fe-abed 595 | |= [p=ship s=space a=app b=bucket l=log] 596 | =/ tod (~(got bi tome) [p s] a) 597 | =/ fee (~(got by feed.tod) [b l]) 598 | =/ pp `@tas`(scot %p p) 599 | =/ suv (wood s) 600 | =/ type ?:(=(l %.y) %log %feed) 601 | %= fe 602 | shi p 603 | spa s 604 | ap a 605 | buc b 606 | lo l 607 | tod tod 608 | per perm.fee 609 | ids ids.fee 610 | inv invites.fee 611 | data data.fee 612 | data-pax /feed/[pp]/[suv]/[a]/[b]/[type]/data/all 613 | perm-pax /feed/[pp]/[suv]/[a]/[b]/[type]/perm 614 | == 615 | :: +fe-dude: handle foreign feed updates (facts) 616 | :: 617 | ++ fe-dude 618 | |= cag=cage 619 | ^+ fe 620 | ?< =(our.bol shi) 621 | ?+ p.cag ~|('bad-feed-dude' !!) 622 | %feed-update 623 | =/ upd !<(feed-update q.cag) 624 | =/ fon ((on time feed-value) gth) :: mop needs this to work 625 | ?+ -.upd ~|('bad-feed-update' !!) 626 | %new 627 | %= fe 628 | ids (~(put by ids) id.upd time.upd) 629 | data (put:fon data time.upd [id.upd ship.upd ship.upd time.upd time.upd s+content.upd *links]) 630 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 631 | == 632 | :: 633 | %edit 634 | =/ has (~(has by ids) id.upd) 635 | =/ new-ids :: add new ID if we don't have original 636 | ?: has 637 | ids 638 | (~(put by ids) id.upd time.upd) 639 | :: 640 | =/ og-time (~(got by ids) id.upd) 641 | :: 642 | =/ old-by 643 | ?: has 644 | created-by:(got:fon data og-time) 645 | :: if we receive %edit without an original, just use them as the original author. 646 | ship.upd 647 | :: 648 | %= fe 649 | ids new-ids 650 | data (put:fon data og-time [id.upd old-by ship.upd og-time time.upd s+content.upd *links]) 651 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 652 | == 653 | :: 654 | %delete 655 | :: don't have it, ignore 656 | ?. (~(has by ids) id.upd) fe 657 | =/ res (del:fon data time.upd) 658 | %= fe 659 | ids (~(del by ids) id.upd) 660 | data +.res 661 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 662 | == 663 | :: 664 | %clear 665 | %= fe 666 | ids *feed-ids 667 | data *feed-data 668 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 669 | == 670 | :: 671 | %set-link 672 | :: don't have the post, ignore 673 | ?. (~(has by ids) id.upd) fe 674 | =/ post (got:fon data time.upd) 675 | =. links.post (~(put by links.post) ship.upd s+value.upd) 676 | %= fe 677 | data (put:fon data time.upd post) 678 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 679 | == 680 | :: 681 | %remove-link 682 | :: don't have the post, ignore 683 | ?. (~(has by ids) id.upd) fe 684 | =/ post (got:fon data time.upd) 685 | :: don't have the link, ignore 686 | ?. (~(has by links.post) ship.upd) fe 687 | =. links.post (~(del by links.post) ship.upd) 688 | %= fe 689 | data (put:fon data time.upd post) 690 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 691 | == 692 | :: 693 | %all 694 | %= fe 695 | ids (malt (turn (tap:fon data.upd) |=([=time =feed-value] [id.feed-value time]))) 696 | data data.upd 697 | caz [[%give %fact ~[data-pax] %feed-update !>(upd)] caz] 698 | == 699 | :: 700 | %perm 701 | =/ lc :~ [%give %fact ~[perm-pax] %feed-update !>(upd)] 702 | [%pass perm-pax %agent [shi %tome] %leave ~] 703 | == 704 | %= fe 705 | per [read=%yes +.upd] 706 | caz (welp lc caz) 707 | == 708 | :: 709 | == 710 | == 711 | 712 | :: +fe-watch: handle incoming watch requests 713 | :: 714 | ++ fe-watch 715 | |= rest=(pole knot) 716 | ^+ fe 717 | ?> (fe-perm %read) 718 | ?+ rest ~|(bad-feed-watch-path/rest !!) 719 | [%perm ~] 720 | %- fe-emit 721 | [%give %fact ~ %feed-update !>(`feed-update`fe-team)] 722 | :: 723 | [%data %all ~] 724 | %- fe-emit 725 | [%give %fact ~ %feed-update !>(`feed-update`[%all data])] 726 | :: 727 | == 728 | :: +fe-poke: handle log/feed pokes 729 | :: 730 | ++ fe-poke 731 | |= act=feed-action 732 | ^+ fe 733 | =/ fon ((on time feed-value) gth) :: mop needs this to work 734 | ?+ -.act ~|('bad-feed-action' !!) 735 | %new-post 736 | ?> (fe-perm %create) 737 | :: 738 | %= fe 739 | ids (~(put by ids) id.act now.bol) 740 | data (put:fon data now.bol [id.act src.bol src.bol now.bol now.bol s+content.act *links]) 741 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%new id.act now.bol src.bol content.act])] caz] 742 | == 743 | :: 744 | %edit-post 745 | =+ time=(~(gut by ids) id.act ~) 746 | ?~ time 747 | ?> (fe-perm ?:(=(lo %.y) %overwrite %create)) 748 | ~|('no-post-to-edit' !!) 749 | :: 750 | =/ curr (got:fon data time) 751 | =* lvl ?:(=(src.bol created-by.curr) ?:(=(lo %.y) %overwrite %create) %overwrite) 752 | ?> (fe-perm lvl) 753 | :: 754 | %= fe 755 | data (put:fon data time [id.act created-by.curr src.bol created-at.curr now.bol s+content.act *links]) 756 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%edit id.act now.bol src.bol content.act])] caz] 757 | == 758 | :: 759 | %delete-post 760 | =+ time=(~(gut by ids) id.act ~) 761 | ?~ time :: if no post, do nothing 762 | ?> (fe-perm ?:(=(lo %.y) %overwrite %create)) 763 | fe 764 | :: 765 | =* curr (got:fon data time) 766 | =* lvl ?:(=(src.bol created-by.curr) ?:(=(lo %.y) %overwrite %create) %overwrite) 767 | ?> (fe-perm lvl) 768 | :: 769 | =/ res (del:fon data time) 770 | %= fe 771 | ids (~(del by ids) id.act) 772 | data +.res :: mop delete return type is weird. tail is the new map 773 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%delete id.act time])] caz] 774 | == 775 | :: 776 | %clear-feed 777 | ?> (fe-perm %overwrite) 778 | ?~ ids fe :: if no posts, do nothing 779 | :: 780 | %= fe 781 | ids *feed-ids 782 | data *feed-data 783 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%clear ~])] caz] 784 | == 785 | :: 786 | %verify-feed 787 | :: The bucket must exist to get this far, so we just need to verify read permissions. 788 | ?> (fe-perm %read) 789 | fe 790 | :: 791 | %perm-feed 792 | :: force everyone to re-subscribe 793 | fe(per perm.act, caz [[%give %kick ~[data-pax] ~] caz]) 794 | :: 795 | %invite-feed 796 | =/ guy `@p`(slav %p guy.act) 797 | %= fe 798 | inv (~(put by inv) guy level.act) 799 | caz [[%give %kick ~[data-pax] `guy] caz] 800 | == 801 | :: 802 | %set-post-link :: links are currently only supported by feeds, not logs 803 | ?> =(lo %.n) 804 | ?> (fe-perm %create) 805 | =+ time=(~(gut by ids) id.act ~) 806 | ?~ time ~|('no-post-for-set-link' !!) 807 | :: 808 | =/ curr (got:fon data time) 809 | =/ ship-str `@t`(scot %p src.bol) 810 | =/ new-links (~(put by links.curr) ship-str s+value.act) 811 | %= fe 812 | data (put:fon data time [id.act created-by.curr updated-by.curr created-at.curr updated-at.curr content.curr new-links]) 813 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%set-link id.act time ship-str value.act])] caz] 814 | == 815 | :: 816 | %remove-post-link 817 | ?> =(lo %.n) 818 | ?> (fe-perm %create) 819 | =+ time=(~(gut by ids) id.act ~) 820 | ?~ time :: if no post, do nothing. 821 | fe 822 | :: 823 | =/ curr (got:fon data time) 824 | =/ ship-str `@t`(scot %p src.bol) 825 | =/ new-links (~(del by links.curr) ship-str) 826 | %= fe 827 | data (put:fon data time [id.act created-by.curr updated-by.curr created-at.curr updated-at.curr content.curr new-links]) 828 | caz [[%give %fact ~[data-pax] %feed-update !>(`feed-update`[%remove-link id.act time ship-str])] caz] 829 | == 830 | :: 831 | == 832 | :: +fe-peek: handle kv peek requests 833 | :: 834 | ++ fe-peek 835 | |= rest=(pole knot) 836 | ^- (unit (unit cage)) 837 | :: no perms check since no remote scry 838 | ?+ rest ~|(bad-feed-peek-path/rest !!) 839 | [%data %all ~] 840 | ``feed-update+!>(`feed-update`[%all data]) 841 | :: 842 | [%data %key id=@t ~] 843 | =/ fon ((on time feed-value) gth) 844 | =/ time (~(gut by ids) id.rest ~) 845 | =/ post 846 | ?~ time ~ 847 | (got:fon data time) 848 | :: 849 | ``feed-update+!>(`feed-update`[%get post]) 850 | :: 851 | == 852 | :: fe-view: start watching foreign feed 853 | :: 854 | ++ fe-view 855 | |= rest=(pole knot) 856 | ^+ fe 857 | ?+ rest ~|(bad-feed-watch-path/rest !!) 858 | [%perm ~] 859 | (fe-emit [%pass perm-pax %agent [shi %tome] %watch perm-pax]) 860 | :: 861 | [%data %all ~] 862 | ?: (~(has in subs) data-pax) fe 863 | (fe-emit [%pass data-pax %agent [shi %tome] %watch data-pax]) 864 | :: 865 | == 866 | :: +fe-perm: check a permission level, return true if allowed 867 | :: duplicates +kv-perm 868 | ++ fe-perm 869 | |= [lvl=?(%read %create %overwrite)] 870 | ^- ? 871 | ?: =(src.bol our.bol) %.y :: always allow local 872 | =/ bro (~(gut by inv) src.bol ~) 873 | ?- lvl 874 | %read 875 | ?~ bro 876 | ?- read.per 877 | %unset %.n 878 | %no %.n 879 | %our %.n :: it's not us, so no. 880 | %open %.y 881 | %yes %.y 882 | %space 883 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun) 884 | ?> ?=(%is-member -.memb) 885 | is-member.memb 886 | == 887 | :: use invite level to determine 888 | ?:(?=(%block bro) %.n %.y) 889 | :: 890 | %create 891 | ?~ bro 892 | ?- write.per 893 | %unset %.n 894 | %no %.n 895 | %our %.n 896 | %open %.y 897 | %yes %.y 898 | %space 899 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun) 900 | ?> ?=(%is-member -.memb) 901 | is-member.memb 902 | == 903 | ?:(?=(?(%block %read) bro) %.n %.y) 904 | :: 905 | %overwrite 906 | ?~ bro 907 | ?- admin.per 908 | %unset %.n 909 | %no %.n 910 | %our %.n 911 | %open %.y 912 | %yes %.y 913 | %space 914 | =/ memb .^(view:m-s:r-l %gx /(scot %p our.bol)/spaces/(scot %da now.bol)/(scot %p shi)/[spa]/is-member/(scot %p our.bol)/noun) 915 | ?> ?=(%is-member -.memb) 916 | is-member.memb 917 | == 918 | ?:(?=(?(%block %read %write) bro) %.n %.y) 919 | :: 920 | == 921 | :: +fe-team: get read/write/admin permissions for a ship 922 | :: 923 | ++ fe-team 924 | =/ write ?:((fe-perm %create) %yes %no) 925 | =/ admin ?:((fe-perm %overwrite) %yes %no) 926 | [%perm write admin] 927 | :: 928 | -- 929 | -- -------------------------------------------------------------------------------- /desk/desk.bill: -------------------------------------------------------------------------------- 1 | :~ %tome 2 | == -------------------------------------------------------------------------------- /desk/desk.docket-0: -------------------------------------------------------------------------------- 1 | :~ title+'Tome DB' 2 | info+'Composable database primitive for Urbit' 3 | color+0xce.bef0 4 | version+[1 0 4] 5 | website+'https://docs.holium.com/tomedb/' 6 | license+'MIT' 7 | glob-http+['https://holium-app-globs.nyc3.cdn.digitaloceanspaces.com/realm%2Fholium-com-redirect.glob' 0v4.39m1m.t5rlt.d94cv.akdr1.389ub] 8 | base+'tome-db' 9 | == 10 | -------------------------------------------------------------------------------- /desk/desk.ship: -------------------------------------------------------------------------------- 1 | ~hostyv -------------------------------------------------------------------------------- /desk/lib/dbug.hoon: -------------------------------------------------------------------------------- 1 | :: dbug: agent wrapper for generic debugging tools 2 | :: 3 | :: usage: %-(agent:dbug your-agent) 4 | :: 5 | |% 6 | +$ poke 7 | $% [%bowl ~] 8 | [%state grab=cord] 9 | [%incoming =about] 10 | [%outgoing =about] 11 | == 12 | :: 13 | +$ about 14 | $@ ~ 15 | $% [%ship =ship] 16 | [%path =path] 17 | [%wire =wire] 18 | [%term =term] 19 | == 20 | :: 21 | ++ agent 22 | |= =agent:gall 23 | ^- agent:gall 24 | !. 25 | |_ =bowl:gall 26 | +* this . 27 | ag ~(. agent bowl) 28 | :: 29 | ++ on-poke 30 | |= [=mark =vase] 31 | ^- (quip card:agent:gall agent:gall) 32 | ?. ?=(%dbug mark) 33 | =^ cards agent (on-poke:ag mark vase) 34 | [cards this] 35 | =/ dbug 36 | !<(poke vase) 37 | =; =tang 38 | ((%*(. slog pri 1) tang) [~ this]) 39 | ?- -.dbug 40 | %bowl [(sell !>(bowl))]~ 41 | :: 42 | %state 43 | =? grab.dbug =('' grab.dbug) '-' 44 | =; product=^vase 45 | [(sell product)]~ 46 | =/ state=^vase 47 | :: if the underlying app has implemented a /dbug/state scry endpoint, 48 | :: use that vase in place of +on-save's. 49 | :: 50 | =/ result=(each ^vase tang) 51 | (mule |.(q:(need (need (on-peek:ag /x/dbug/state))))) 52 | ?:(?=(%& -.result) p.result on-save:ag) 53 | %+ slap 54 | (slop state !>([bowl=bowl ..zuse])) 55 | (ream grab.dbug) 56 | :: 57 | %incoming 58 | =; =tang 59 | ?^ tang tang 60 | [%leaf "no matching subscriptions"]~ 61 | %+ murn 62 | %+ sort ~(tap by sup.bowl) 63 | |= [[* a=[=ship =path]] [* b=[=ship =path]]] 64 | (aor [path ship]:a [path ship]:b) 65 | |= [=duct [=ship =path]] 66 | ^- (unit tank) 67 | =; relevant=? 68 | ?. relevant ~ 69 | `>[path=path from=ship duct=duct]< 70 | ?: ?=(~ about.dbug) & 71 | ?- -.about.dbug 72 | %ship =(ship ship.about.dbug) 73 | %path ?=(^ (find path.about.dbug path)) 74 | %wire %+ lien duct 75 | |=(=wire ?=(^ (find wire.about.dbug wire))) 76 | %term !! 77 | == 78 | :: 79 | %outgoing 80 | =; =tang 81 | ?^ tang tang 82 | [%leaf "no matching subscriptions"]~ 83 | %+ murn 84 | %+ sort ~(tap by wex.bowl) 85 | |= [[[a=wire *] *] [[b=wire *] *]] 86 | (aor a b) 87 | |= [[=wire =ship =term] [acked=? =path]] 88 | ^- (unit tank) 89 | =; relevant=? 90 | ?. relevant ~ 91 | `>[wire=wire agnt=[ship term] path=path ackd=acked]< 92 | ?: ?=(~ about.dbug) & 93 | ?- -.about.dbug 94 | %ship =(ship ship.about.dbug) 95 | %path ?=(^ (find path.about.dbug path)) 96 | %wire ?=(^ (find wire.about.dbug wire)) 97 | %term =(term term.about.dbug) 98 | == 99 | == 100 | :: 101 | ++ on-peek 102 | |= =path 103 | ^- (unit (unit cage)) 104 | ?. ?=([@ %dbug *] path) 105 | (on-peek:ag path) 106 | ?+ path [~ ~] 107 | [%u %dbug ~] ``noun+!>(&) 108 | [%x %dbug %state ~] ``noun+!>(on-save:ag) 109 | [%x %dbug %subscriptions ~] ``noun+!>([wex sup]:bowl) 110 | == 111 | :: 112 | ++ on-init 113 | ^- (quip card:agent:gall agent:gall) 114 | =^ cards agent on-init:ag 115 | [cards this] 116 | :: 117 | ++ on-save on-save:ag 118 | :: 119 | ++ on-load 120 | |= old-state=vase 121 | ^- (quip card:agent:gall agent:gall) 122 | =^ cards agent (on-load:ag old-state) 123 | [cards this] 124 | :: 125 | ++ on-watch 126 | |= =path 127 | ^- (quip card:agent:gall agent:gall) 128 | =^ cards agent (on-watch:ag path) 129 | [cards this] 130 | :: 131 | ++ on-leave 132 | |= =path 133 | ^- (quip card:agent:gall agent:gall) 134 | =^ cards agent (on-leave:ag path) 135 | [cards this] 136 | :: 137 | ++ on-agent 138 | |= [=wire =sign:agent:gall] 139 | ^- (quip card:agent:gall agent:gall) 140 | =^ cards agent (on-agent:ag wire sign) 141 | [cards this] 142 | :: 143 | ++ on-arvo 144 | |= [=wire =sign-arvo] 145 | ^- (quip card:agent:gall agent:gall) 146 | =^ cards agent (on-arvo:ag wire sign-arvo) 147 | [cards this] 148 | :: 149 | ++ on-fail 150 | |= [=term =tang] 151 | ^- (quip card:agent:gall agent:gall) 152 | =^ cards agent (on-fail:ag term tang) 153 | [cards this] 154 | -- 155 | -- 156 | -------------------------------------------------------------------------------- /desk/lib/default-agent.hoon: -------------------------------------------------------------------------------- 1 | /+ skeleton 2 | |* [agent=* help=*] 3 | ?: ?=(%& help) 4 | ~| %default-agent-helpfully-crashing 5 | skeleton 6 | |_ =bowl:gall 7 | ++ on-init 8 | `agent 9 | :: 10 | ++ on-save 11 | !>(~) 12 | :: 13 | ++ on-load 14 | |= old-state=vase 15 | `agent 16 | :: 17 | ++ on-poke 18 | |= =cage 19 | ~| "unexpected poke to {} with mark {}" 20 | !! 21 | :: 22 | ++ on-watch 23 | |= =path 24 | ~| "unexpected subscription to {} on path {}" 25 | !! 26 | :: 27 | ++ on-leave 28 | |= path 29 | `agent 30 | :: 31 | ++ on-peek 32 | |= =path 33 | ~| "unexpected scry into {} on path {}" 34 | !! 35 | :: 36 | ++ on-agent 37 | |= [=wire =sign:agent:gall] 38 | ^- (quip card:agent:gall _agent) 39 | ?- -.sign 40 | %poke-ack 41 | ?~ p.sign 42 | `agent 43 | %- (slog leaf+"poke failed from {} on wire {}" u.p.sign) 44 | `agent 45 | :: 46 | %watch-ack 47 | ?~ p.sign 48 | `agent 49 | =/ =tank leaf+"subscribe failed from {} on wire {}" 50 | %- (slog tank u.p.sign) 51 | `agent 52 | :: 53 | %kick `agent 54 | %fact 55 | ~| "unexpected subscription update to {} on wire {}" 56 | ~| "with mark {}" 57 | !! 58 | == 59 | :: 60 | ++ on-arvo 61 | |= [=wire =sign-arvo] 62 | ~| "unexpected system response {<-.sign-arvo>} to {} on wire {}" 63 | !! 64 | :: 65 | ++ on-fail 66 | |= [=term =tang] 67 | %- (slog leaf+"error in {}" >term< tang) 68 | `agent 69 | -- 70 | -------------------------------------------------------------------------------- /desk/lib/docket.hoon: -------------------------------------------------------------------------------- 1 | /- *docket 2 | |% 3 | :: 4 | ++ mime 5 | |% 6 | +$ draft 7 | $: title=(unit @t) 8 | info=(unit @t) 9 | color=(unit @ux) 10 | glob-http=(unit [=url hash=@uvH]) 11 | glob-ames=(unit [=ship hash=@uvH]) 12 | base=(unit term) 13 | site=(unit path) 14 | image=(unit url) 15 | version=(unit version) 16 | website=(unit url) 17 | license=(unit cord) 18 | == 19 | :: 20 | ++ finalize 21 | |= =draft 22 | ^- (unit docket) 23 | ?~ title.draft ~ 24 | ?~ info.draft ~ 25 | ?~ color.draft ~ 26 | ?~ version.draft ~ 27 | ?~ website.draft ~ 28 | ?~ license.draft ~ 29 | =/ href=(unit href) 30 | ?^ site.draft `[%site u.site.draft] 31 | ?~ base.draft ~ 32 | ?^ glob-http.draft 33 | `[%glob u.base hash.u.glob-http %http url.u.glob-http]:draft 34 | ?~ glob-ames.draft 35 | ~ 36 | `[%glob u.base hash.u.glob-ames %ames ship.u.glob-ames]:draft 37 | ?~ href ~ 38 | =, draft 39 | :- ~ 40 | :* %1 41 | u.title 42 | u.info 43 | u.color 44 | u.href 45 | image 46 | u.version 47 | u.website 48 | u.license 49 | == 50 | :: 51 | ++ from-clauses 52 | =| =draft 53 | |= cls=(list clause) 54 | ^- (unit docket) 55 | =* loop $ 56 | ?~ cls (finalize draft) 57 | =* clause i.cls 58 | =. draft 59 | ?- -.clause 60 | %title draft(title `title.clause) 61 | %info draft(info `info.clause) 62 | %color draft(color `color.clause) 63 | %glob-http draft(glob-http `[url hash]:clause) 64 | %glob-ames draft(glob-ames `[ship hash]:clause) 65 | %base draft(base `base.clause) 66 | %site draft(site `path.clause) 67 | %image draft(image `url.clause) 68 | %version draft(version `version.clause) 69 | %website draft(website `website.clause) 70 | %license draft(license `license.clause) 71 | == 72 | loop(cls t.cls) 73 | :: 74 | ++ to-clauses 75 | |= d=docket 76 | ^- (list clause) 77 | %- zing 78 | :~ :~ title+title.d 79 | info+info.d 80 | color+color.d 81 | version+version.d 82 | website+website.d 83 | license+license.d 84 | == 85 | ?~ image.d ~ ~[image+u.image.d] 86 | ?: ?=(%site -.href.d) ~[site+path.href.d] 87 | =/ ref=glob-reference glob-reference.href.d 88 | :~ base+base.href.d 89 | ?- -.location.ref 90 | %http [%glob-http url.location.ref hash.ref] 91 | %ames [%glob-ames ship.location.ref hash.ref] 92 | == == == 93 | :: 94 | ++ spit-clause 95 | |= =clause 96 | ^- tape 97 | %+ weld " {(trip -.clause)}+" 98 | ?+ -.clause "'{(trip +.clause)}'" 99 | %color (scow %ux color.clause) 100 | %site (spud path.clause) 101 | :: 102 | %glob-http 103 | "['{(trip url.clause)}' {(scow %uv hash.clause)}]" 104 | :: 105 | %glob-ames 106 | "[{(scow %p ship.clause)} {(scow %uv hash.clause)}]" 107 | :: 108 | %version 109 | =, version.clause 110 | "[{(scow %ud major)} {(scow %ud minor)} {(scow %ud patch)}]" 111 | == 112 | :: 113 | ++ spit-docket 114 | |= dock=docket 115 | ^- tape 116 | ;: welp 117 | ":~\0a" 118 | `tape`(zing (join "\0a" (turn (to-clauses dock) spit-clause))) 119 | "\0a==" 120 | == 121 | -- 122 | :: 123 | ++ enjs 124 | =, enjs:format 125 | |% 126 | :: 127 | ++ charge-update 128 | |= u=^charge-update 129 | ^- json 130 | %+ frond -.u 131 | ^- json 132 | ?- -.u 133 | %del-charge s+desk.u 134 | :: 135 | %initial 136 | %- pairs 137 | %+ turn ~(tap by initial.u) 138 | |=([=desk c=^charge] [desk (charge c)]) 139 | :: 140 | %add-charge 141 | %- pairs 142 | :~ desk+s+desk.u 143 | charge+(charge charge.u) 144 | == 145 | == 146 | :: 147 | ++ num 148 | |= a=@u 149 | ^- ^tape 150 | =/ p=json (numb a) 151 | ?> ?=(%n -.p) 152 | (trip p.p) 153 | :: 154 | ++ version 155 | |= v=^version 156 | ^- json 157 | :- %s 158 | %- crip 159 | "{(num major.v)}.{(num minor.v)}.{(num patch.v)}" 160 | :: 161 | ++ merge 162 | |= [a=json b=json] 163 | ^- json 164 | ?> &(?=(%o -.a) ?=(%o -.b)) 165 | [%o (~(uni by p.a) p.b)] 166 | :: 167 | ++ href 168 | |= h=^href 169 | %+ frond -.h 170 | ?- -.h 171 | %site s+(spat path.h) 172 | %glob 173 | %- pairs 174 | :~ base+s+base.h 175 | glob-reference+(glob-reference glob-reference.h) 176 | == 177 | == 178 | :: 179 | ++ glob-reference 180 | |= ref=^glob-reference 181 | %- pairs 182 | :~ hash+s+(scot %uv hash.ref) 183 | location+(glob-location location.ref) 184 | == 185 | :: 186 | ++ glob-location 187 | |= loc=^glob-location 188 | ^- json 189 | %+ frond -.loc 190 | ?- -.loc 191 | %http s+url.loc 192 | %ames s+(scot %p ship.loc) 193 | == 194 | :: 195 | ++ charge 196 | |= c=^charge 197 | %+ merge (docket docket.c) 198 | %- pairs 199 | :~ chad+(chad chad.c) 200 | == 201 | :: 202 | ++ docket 203 | |= d=^docket 204 | ^- json 205 | %- pairs 206 | :~ title+s+title.d 207 | info+s+info.d 208 | color+s+(scot %ux color.d) 209 | href+(href href.d) 210 | image+?~(image.d ~ s+u.image.d) 211 | version+(version version.d) 212 | license+s+license.d 213 | website+s+website.d 214 | == 215 | :: 216 | ++ chad 217 | |= c=^chad 218 | %+ frond -.c 219 | ?+ -.c ~ 220 | %hung s+err.c 221 | == 222 | -- 223 | -- 224 | -------------------------------------------------------------------------------- /desk/lib/mip.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | ++ mip :: map of maps 3 | |$ [kex key value] 4 | (map kex (map key value)) 5 | :: 6 | ++ bi :: mip engine 7 | =| a=(map * (map)) 8 | |@ 9 | ++ del 10 | |* [b=* c=*] 11 | =+ d=(~(gut by a) b ~) 12 | =+ e=(~(del by d) c) 13 | ?~ e 14 | (~(del by a) b) 15 | (~(put by a) b e) 16 | :: 17 | ++ get 18 | |* [b=* c=*] 19 | => .(b `_?>(?=(^ a) p.n.a)`b, c `_?>(?=(^ a) ?>(?=(^ q.n.a) p.n.q.n.a))`c) 20 | ^- (unit _?>(?=(^ a) ?>(?=(^ q.n.a) q.n.q.n.a))) 21 | (~(get by (~(gut by a) b ~)) c) 22 | :: 23 | ++ got 24 | |* [b=* c=*] 25 | (need (get b c)) 26 | :: 27 | ++ gut 28 | |* [b=* c=* d=*] 29 | (~(gut by (~(gut by a) b ~)) c d) 30 | :: 31 | ++ has 32 | |* [b=* c=*] 33 | !=(~ (get b c)) 34 | :: 35 | ++ key 36 | |* b=* 37 | ~(key by (~(gut by a) b ~)) 38 | :: 39 | ++ put 40 | |* [b=* c=* d=*] 41 | %+ ~(put by a) b 42 | %. [c d] 43 | %~ put by 44 | (~(gut by a) b ~) 45 | :: 46 | ++ tap 47 | ::NOTE naive turn-based implementation find-errors ): 48 | =< $ 49 | =+ b=`_?>(?=(^ a) *(list [x=_p.n.a _?>(?=(^ q.n.a) [y=p v=q]:n.q.n.a)]))`~ 50 | |. ^+ b 51 | ?~ a 52 | b 53 | $(a r.a, b (welp (turn ~(tap by q.n.a) (lead p.n.a)) $(a l.a))) 54 | -- 55 | -- 56 | -------------------------------------------------------------------------------- /desk/lib/realm-lib.hoon: -------------------------------------------------------------------------------- 1 | /- s-p=spaces-path, s-s=spaces-store 2 | /- m-s=membership, v-s=visas 3 | :: 4 | |% 5 | +$ spat (pair ship cord) 6 | +$ space space:s-s 7 | +$ stype type:s-s 8 | +$ spaces spaces:s-s 9 | +$ member member:m-s 10 | +$ members members:m-s 11 | +$ invitations invitations:v-s 12 | :: 13 | ++ realms 14 | |_ $: dish=bowl:gall 15 | inis=(set ship) 16 | mems=(set ship) 17 | adms=(set ship) 18 | owns=(set ship) 19 | pend=(set ship) 20 | hust=(unit ship) 21 | sput=(unit spat) 22 | spuc=(unit space) 23 | spaz=spaces 24 | == 25 | +* re . 26 | pat /(scot %p our.dish)/spaces/(scot %da now.dish) 27 | ++ re-abet-saz `spaces`spaz :: get spaces per query 28 | ++ re-abet-pen `(set ship)`pend :: get pending members 29 | ++ re-abet-ini `(set ship)`inis :: get initiates 30 | ++ re-abet-mem `(set ship)`mems :: get members 31 | ++ re-abet-adm `(set ship)`adms :: get administrators 32 | ++ re-abet-own `(set ship)`owns :: get owners 33 | ++ re-abet-hos `ship`(need hust) :: get host 34 | ++ re-abet-sap `spat`(need sput) :: get space-path 35 | ++ re-abet-det `space`(need spuc) :: get space details 36 | ++ all 37 | ^- spaces 38 | =- ?>(?=([%spaces *] -) +.-) 39 | .^(view:s-s %gx (welp pat /all/noun)) 40 | ++ inv 41 | ^- invitations 42 | =- ?>(?=([%invitations *] -) +.-) 43 | .^(view:v-s %gx (welp pat /invitations/noun)) 44 | ++ mem 45 | =+ spa=(need sput) 46 | ^- members 47 | =- ?>(?=([%members *] -) +.-) 48 | .^ view:m-s 49 | %gx 50 | (welp pat /(scot %p p.spa)/[q.spa]/members/noun) 51 | == 52 | ++ re-read 53 | ^+ re 54 | =- re(spaz -) 55 | %- ~(rep by all) 56 | |= [sap=(pair spat space) suz=spaces] 57 | ?.(=((need hust) p.p.sap) suz (~(put by suz) sap)) 58 | ++ re-abed 59 | |= [bol=bowl:gall and=(each (pair ship cord) (unit ship))] 60 | ^+ re 61 | =. dish bol 62 | ?: ?=(%.y -.and) 63 | re-team:re-read(sput `p.and, hust `p.p.and) 64 | ?~(p.and re(spaz all) re-read(hust p.and)) 65 | ++ re-team 66 | ^+ re 67 | =; [i=(set @p) m=(set @p) a=(set @p) o=(set @p) p=(set @p)] 68 | re(inis i, mems m, adms a, owns o, pend p) 69 | %- ~(rep by mem) 70 | |= $: [s=ship m=member] 71 | $= o 72 | $: i=(set @p) 73 | m=(set @p) 74 | a=(set @p) 75 | o=(set @p) 76 | p=(set @p) 77 | == 78 | == 79 | ?: =(%invited status.m) [i.o m.o a.o o.o (~(put in p.o) s)] 80 | =: i.o ?.((~(has in roles.m) %initiate) i.o (~(put in i.o) s)) 81 | m.o ?.((~(has in roles.m) %member) m.o (~(put in m.o) s)) 82 | a.o ?.((~(has in roles.m) %admin) a.o (~(put in a.o) s)) 83 | o.o ?.((~(has in roles.m) %owner) o.o (~(put in o.o) s)) 84 | == 85 | [i.o m.o a.o o.o p.o] 86 | -- 87 | -- 88 | -------------------------------------------------------------------------------- /desk/lib/skeleton.hoon: -------------------------------------------------------------------------------- 1 | :: Similar to default-agent except crashes everywhere 2 | ^- agent:gall 3 | |_ bowl:gall 4 | ++ on-init 5 | ^- (quip card:agent:gall agent:gall) 6 | !! 7 | :: 8 | ++ on-save 9 | ^- vase 10 | !! 11 | :: 12 | ++ on-load 13 | |~ old-state=vase 14 | ^- (quip card:agent:gall agent:gall) 15 | !! 16 | :: 17 | ++ on-poke 18 | |~ in-poke-data=cage 19 | ^- (quip card:agent:gall agent:gall) 20 | !! 21 | :: 22 | ++ on-watch 23 | |~ path 24 | ^- (quip card:agent:gall agent:gall) 25 | !! 26 | :: 27 | ++ on-leave 28 | |~ path 29 | ^- (quip card:agent:gall agent:gall) 30 | !! 31 | :: 32 | ++ on-peek 33 | |~ path 34 | ^- (unit (unit cage)) 35 | !! 36 | :: 37 | ++ on-agent 38 | |~ [wire sign:agent:gall] 39 | ^- (quip card:agent:gall agent:gall) 40 | !! 41 | :: 42 | ++ on-arvo 43 | |~ [wire =sign-arvo] 44 | ^- (quip card:agent:gall agent:gall) 45 | !! 46 | :: 47 | ++ on-fail 48 | |~ [term tang] 49 | ^- (quip card:agent:gall agent:gall) 50 | !! 51 | -- 52 | -------------------------------------------------------------------------------- /desk/lib/spaces.hoon: -------------------------------------------------------------------------------- 1 | /- store=spaces-store, member-store=membership, visas 2 | /+ memb-lib=membership 3 | =< [store .] 4 | =, store 5 | |% 6 | 7 | ++ create-space 8 | |= [=ship slug=@t payload=add-payload:store updated-at=@da] 9 | ^- space:store 10 | =/ default-theme 11 | [ 12 | mode=%light 13 | background-color='#C4C3BF' 14 | accent-color='#4E9EFD' 15 | input-color='#fff' 16 | dock-color='#fff' 17 | icon-color='rgba(95,94,88,0.3)' 18 | text-color='#333333' 19 | window-color='#fff' 20 | wallpaper='https://images.unsplash.com/photo-1622547748225-3fc4abd2cca0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2832&q=100' 21 | ] 22 | =/ new-space 23 | [ 24 | path=[ship slug] 25 | name=name:payload 26 | description=description:payload 27 | type=type:payload 28 | access=access:payload 29 | picture=picture:payload 30 | color=color:payload 31 | archetype=archetype:payload 32 | theme=default-theme 33 | updated-at=updated-at 34 | ] 35 | new-space 36 | :: 37 | :: json 38 | :: 39 | ++ enjs 40 | =, enjs:format 41 | |% 42 | ++ reaction 43 | |= rct=^reaction 44 | ^- json 45 | %- pairs 46 | :_ ~ 47 | ^- [cord json] 48 | ?- -.rct 49 | %initial 50 | :- %initial 51 | %- pairs 52 | :~ [%spaces (spaces-map:encode spaces.rct)] 53 | [%membership (membership-map:encode membership.rct)] 54 | [%invitations (invitations:encode invitations.rct)] 55 | == 56 | :: 57 | %add 58 | :- %add 59 | %- pairs 60 | :~ [%space (spc:encode space.rct)] 61 | [%members (membs:encode members.rct)] 62 | == 63 | :: 64 | %replace 65 | :- %replace 66 | %- pairs 67 | :~ [%space (spc:encode space.rct)] 68 | == 69 | :: 70 | %remove 71 | :- %remove 72 | %- pairs 73 | :~ [%space-path s+(spat /(scot %p ship.path.rct)/(scot %tas space.path.rct))] 74 | == 75 | :: 76 | %remote-space 77 | :- %remote-space 78 | %- pairs 79 | :~ [%path s+(spat /(scot %p ship.path.rct)/(scot %tas space.path.rct))] 80 | [%space (spc:encode space.rct)] 81 | :: [%members (passes:encode:membership membership.rct)] 82 | [%members (membs:encode members.rct)] 83 | == 84 | 85 | 86 | :: %members 87 | :: :- %members 88 | :: %- pairs 89 | :: :~ [%path s+(spat /(scot %p ship.path.rct)/(scot %tas space.path.rct))] 90 | :: [%members (membership-json:encode:memb-lib membership.rct)] 91 | :: == 92 | == 93 | :: 94 | ++ view :: encodes for on-peek 95 | |= vi=^view 96 | ^- json 97 | %- pairs 98 | :_ ~ 99 | ^- [cord json] 100 | :- -.vi 101 | ?- -.vi 102 | :: 103 | %space 104 | (spc:encode space.vi) 105 | :: 106 | %spaces 107 | (spaces-map:encode spaces.vi) 108 | == 109 | -- 110 | 111 | :: 112 | ++ dejs 113 | =, dejs:format 114 | |% 115 | ++ action 116 | |= jon=json 117 | ^- ^action 118 | =< (decode jon) 119 | |% 120 | ++ decode 121 | %- of 122 | :~ [%add add-space] 123 | [%update update-space] 124 | [%remove path-key] 125 | [%join path-key] 126 | [%leave path-key] 127 | :: [%kicked kicked] 128 | == 129 | :: 130 | ++ de-space 131 | %- ot 132 | :~ [%path pth] 133 | [%name so] 134 | [%type space-type] 135 | [%access access] 136 | [%picture so] 137 | [%color so] 138 | [%archetype archetype] 139 | [%theme thm] 140 | [%updated-at di] 141 | == 142 | :: 143 | ++ add-space 144 | %- ot 145 | :~ [%slug so] 146 | [%payload add-payload] 147 | [%members (op ;~(pfix sig fed:ag) memb)] 148 | == 149 | :: 150 | ++ update-space 151 | %- ot 152 | :~ [%path pth] 153 | [%payload edit-payload] 154 | == 155 | :: 156 | ++ kicked 157 | %- ot 158 | :~ [%path pth] 159 | [%ship (su ;~(pfix sig fed:ag))] 160 | == 161 | :: 162 | ++ path-key 163 | %- ot 164 | :~ [%path pth] 165 | == 166 | :: 167 | ++ pth 168 | %- ot 169 | :~ [%ship (su ;~(pfix sig fed:ag))] 170 | [%space so] 171 | == 172 | :: 173 | ++ add-payload 174 | %- ot 175 | :~ [%name so] 176 | [%description so] 177 | [%type space-type] 178 | [%access access] 179 | [%picture so] 180 | [%color so] 181 | [%archetype archetype] 182 | == 183 | :: 184 | ++ edit-payload 185 | %- ot 186 | :~ [%name so] 187 | [%description so] 188 | [%access (su (perk %public %private ~))] 189 | [%picture so] 190 | [%color so] 191 | [%theme thm] 192 | == 193 | :: 194 | ++ thm 195 | %- ot 196 | :~ [%mode theme-mode] 197 | [%background-color so] 198 | [%accent-color so] 199 | [%input-color so] 200 | [%dock-color so] 201 | [%icon-color so] 202 | [%text-color so] 203 | [%window-color so] 204 | [%wallpaper so] 205 | == 206 | :: 207 | ++ memb 208 | %- ot 209 | :~ [%roles (as rol)] 210 | [%alias so] 211 | [%status status] 212 | :: [%pinned bo] 213 | == 214 | :: 215 | ++ theme-mode 216 | |= =json 217 | ^- theme-mode:store 218 | ?> ?=(%s -.json) 219 | ?: =('light' p.json) %light 220 | ?: =('dark' p.json) %dark 221 | !! 222 | :: 223 | ++ space-type 224 | |= =json 225 | ^- space-type:store 226 | ?> ?=(%s -.json) 227 | ?: =('group' p.json) %group 228 | ?: =('our' p.json) %our 229 | ?: =('space' p.json) %space 230 | !! 231 | :: 232 | ++ rol 233 | |= =json 234 | ^- role:member-store 235 | ?> ?=(%s -.json) 236 | ?: =('initiate' p.json) %initiate 237 | ?: =('member' p.json) %member 238 | ?: =('admin' p.json) %admin 239 | ?: =('owner' p.json) %owner 240 | !! 241 | :: 242 | ++ archetype 243 | |= =json 244 | ^- archetype:store 245 | ?> ?=(%s -.json) 246 | ?: =('home' p.json) %home 247 | ?: =('community' p.json) %community 248 | ?: =('creator-dao' p.json) %creator-dao 249 | ?: =('service-dao' p.json) %service-dao 250 | ?: =('investment-dao' p.json) %investment-dao 251 | !! 252 | :: 253 | ++ access 254 | |= =json 255 | ^- space-access:store 256 | ?> ?=(%s -.json) 257 | ?: =('public' p.json) %public 258 | ?: =('antechamber' p.json) %antechamber 259 | ?: =('private' p.json) %private 260 | !! 261 | :: 262 | ++ status 263 | |= =json 264 | ^- status:member-store 265 | ?> ?=(%s -.json) 266 | ?: =('invited' p.json) %invited 267 | ?: =('joined' p.json) %joined 268 | ?: =('host' p.json) %host 269 | !! 270 | -- 271 | -- 272 | :: 273 | :: 274 | :: 275 | ++ encode 276 | =, enjs:format 277 | |% 278 | ++ spaces-map 279 | |= =spaces:store 280 | ^- json 281 | %- pairs 282 | %+ turn ~(tap by spaces) 283 | |= [pth=space-path:store space=space:store] 284 | =/ spc-path (spat /(scot %p ship.pth)/(scot %tas space.pth)) 285 | ^- [cord json] 286 | [spc-path (spc space)] 287 | :: 288 | ++ membership-map 289 | |= =membership:member-store 290 | ^- json 291 | %- pairs 292 | %+ turn ~(tap by membership) 293 | |= [pth=space-path:store members=members:member-store] 294 | =/ spc-path (spat /(scot %p ship.pth)/(scot %tas space.pth)) 295 | ^- [cord json] 296 | [spc-path (membs members)] 297 | :: 298 | ++ membs 299 | |= =members:member-store 300 | ^- json 301 | %- pairs 302 | %+ turn ~(tap by members) 303 | |= [=^ship =member:member-store] 304 | ^- [cord json] 305 | [(scot %p ship) (memb member)] 306 | :: 307 | ++ memb 308 | |= =member:member-store 309 | ^- json 310 | %- pairs 311 | :~ ['roles' a+(turn ~(tap in roles.member) |=(rol=role:member-store s+(scot %tas rol)))] 312 | ['status' s+(scot %tas status.member)] 313 | :: ['pinned' b+pinned.member] 314 | == 315 | :: 316 | ++ spc 317 | |= =space 318 | ^- json 319 | %- pairs 320 | :~ ['path' s+(spat /(scot %p ship.path.space)/(scot %tas space.path.space))] 321 | ['name' s+name.space] 322 | ['description' s+description.space] 323 | ['access' s+access.space] 324 | ['type' s+type.space] 325 | ['picture' s+picture.space] 326 | ['color' s+color.space] 327 | ['theme' (thm theme.space)] 328 | ['updatedAt' (time updated-at.space)] 329 | == 330 | :: 331 | ++ thm 332 | |= =theme 333 | ^- json 334 | %- pairs 335 | :~ 336 | ['mode' s+(scot %tas mode.theme)] 337 | ['backgroundColor' s+background-color.theme] 338 | ['accentColor' s+accent-color.theme] 339 | ['inputColor' s+input-color.theme] 340 | ['dockColor' s+dock-color.theme] 341 | ['iconColor' s+icon-color.theme] 342 | ['textColor' s+text-color.theme] 343 | ['windowColor' s+window-color.theme] 344 | ['wallpaper' s+wallpaper.theme] 345 | == 346 | :: 347 | ++ invitations 348 | |= =invitations:visas 349 | ^- json 350 | %- pairs 351 | %+ turn ~(tap by invitations) 352 | |= [pth=space-path:store inv=invite:visas] 353 | =/ spc-path (spat /(scot %p ship.pth)/(scot %tas space.pth)) 354 | ^- [cord json] 355 | [spc-path (invite inv)] 356 | :: 357 | ++ invite 358 | |= =invite:visas 359 | ^- json 360 | %- pairs:enjs:format 361 | :~ ['inviter' s+(scot %p inviter.invite)] 362 | ['path' s+(spat /(scot %p ship.path.invite)/(scot %tas space.path.invite))] 363 | ['role' s+(scot %tas role.invite)] 364 | ['message' s+message.invite] 365 | ['name' s+name.invite] 366 | ['type' s+type.invite] 367 | ['picture' s+picture.invite] 368 | ['color' s+color.invite] 369 | ['invitedAt' (time invited-at.invite)] 370 | == 371 | :: 372 | -- 373 | -- 374 | -------------------------------------------------------------------------------- /desk/lib/strand.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | +$ card card:agent:gall 3 | +$ input 4 | $% [%poke =cage] 5 | [%sign =wire =sign-arvo] 6 | [%agent =wire =sign:agent:gall] 7 | [%watch =path] 8 | == 9 | +$ strand-input [=bowl in=(unit input)] 10 | +$ tid @tatid 11 | +$ bowl 12 | $: our=ship 13 | src=ship 14 | tid=tid 15 | mom=(unit tid) 16 | wex=boat:gall 17 | sup=bitt:gall 18 | eny=@uvJ 19 | now=@da 20 | byk=beak 21 | == 22 | :: 23 | :: cards: cards to send immediately. These will go out even if a 24 | :: later stage of the computation fails, so they shouldn't have 25 | :: any semantic effect on the rest of the system. 26 | :: Alternately, they may record an entry in contracts with 27 | :: enough information to undo the effect if the computation 28 | :: fails. 29 | :: wait: don't move on, stay here. The next sign should come back 30 | :: to this same callback. 31 | :: skip: didn't expect this input; drop it down to be handled 32 | :: elsewhere 33 | :: cont: continue computation with new callback. 34 | :: fail: abort computation; don't send effects 35 | :: done: finish computation; send effects 36 | :: 37 | ++ strand-output-raw 38 | |* a=mold 39 | $~ [~ %done *a] 40 | $: cards=(list card) 41 | $= next 42 | $% [%wait ~] 43 | [%skip ~] 44 | [%cont self=(strand-form-raw a)] 45 | [%fail err=(pair term tang)] 46 | [%done value=a] 47 | == 48 | == 49 | :: 50 | ++ strand-form-raw 51 | |* a=mold 52 | $-(strand-input (strand-output-raw a)) 53 | :: 54 | :: Abort strand computation with error message 55 | :: 56 | ++ strand-fail 57 | |= err=(pair term tang) 58 | |= strand-input 59 | [~ %fail err] 60 | :: 61 | :: Asynchronous transcaction monad. 62 | :: 63 | :: Combo of four monads: 64 | :: - Reader on input 65 | :: - Writer on card 66 | :: - Continuation 67 | :: - Exception 68 | :: 69 | ++ strand 70 | |* a=mold 71 | |% 72 | ++ output (strand-output-raw a) 73 | :: 74 | :: Type of an strand computation. 75 | :: 76 | ++ form (strand-form-raw a) 77 | :: 78 | :: Monadic pure. Identity computation for bind. 79 | :: 80 | ++ pure 81 | |= arg=a 82 | ^- form 83 | |= strand-input 84 | [~ %done arg] 85 | :: 86 | :: Monadic bind. Combines two computations, associatively. 87 | :: 88 | ++ bind 89 | |* b=mold 90 | |= [m-b=(strand-form-raw b) fun=$-(b form)] 91 | ^- form 92 | |= input=strand-input 93 | =/ b-res=(strand-output-raw b) 94 | (m-b input) 95 | ^- output 96 | :- cards.b-res 97 | ?- -.next.b-res 98 | %wait [%wait ~] 99 | %skip [%skip ~] 100 | %cont [%cont ..$(m-b self.next.b-res)] 101 | %fail [%fail err.next.b-res] 102 | %done [%cont (fun value.next.b-res)] 103 | == 104 | :: 105 | :: The strand monad must be evaluted in a particular way to maintain 106 | :: its monadic character. +take:eval implements this. 107 | :: 108 | ++ eval 109 | |% 110 | :: Indelible state of a strand 111 | :: 112 | +$ eval-form 113 | $: =form 114 | == 115 | :: 116 | :: Convert initial form to eval-form 117 | :: 118 | ++ from-form 119 | |= =form 120 | ^- eval-form 121 | form 122 | :: 123 | :: The cases of results of +take 124 | :: 125 | +$ eval-result 126 | $% [%next ~] 127 | [%fail err=(pair term tang)] 128 | [%done value=a] 129 | == 130 | :: 131 | ++ validate-mark 132 | |= [in=* =mark =bowl] 133 | ^- cage 134 | =+ .^ =dais:clay %cb 135 | /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[mark] 136 | == 137 | =/ res (mule |.((vale.dais in))) 138 | ?: ?=(%| -.res) 139 | ~|(%spider-mark-fail (mean leaf+"spider: ames vale fail {}" p.res)) 140 | [mark p.res] 141 | :: 142 | :: Take a new sign and run the strand against it 143 | :: 144 | ++ take 145 | :: cards: accumulate throughout recursion the cards to be 146 | :: produced now 147 | =| cards=(list card) 148 | |= [=eval-form =strand-input] 149 | ^- [[(list card) =eval-result] _eval-form] 150 | =* take-loop $ 151 | =. in.strand-input 152 | ?~ in.strand-input ~ 153 | =/ in u.in.strand-input 154 | ?. ?=(%agent -.in) `in 155 | ?. ?=(%fact -.sign.in) `in 156 | :: 157 | :- ~ 158 | :+ %agent wire.in 159 | [%fact (validate-mark q.q.cage.sign.in p.cage.sign.in bowl.strand-input)] 160 | :: run the strand callback 161 | :: 162 | =/ =output (form.eval-form strand-input) 163 | :: add cards to cards 164 | :: 165 | =. cards 166 | %+ welp 167 | cards 168 | :: XX add tag to wires? 169 | cards.output 170 | :: case-wise handle next steps 171 | :: 172 | ?- -.next.output 173 | %wait [[cards %next ~] eval-form] 174 | %skip [[cards %next ~] eval-form] 175 | %fail [[cards %fail err.next.output] eval-form] 176 | %done [[cards %done value.next.output] eval-form] 177 | %cont 178 | :: recurse to run continuation with initialization input 179 | :: 180 | %_ take-loop 181 | form.eval-form self.next.output 182 | strand-input [bowl.strand-input ~] 183 | == 184 | == 185 | -- 186 | -- 187 | -- 188 | :: 189 | -------------------------------------------------------------------------------- /desk/lib/strandio.hoon: -------------------------------------------------------------------------------- 1 | /- spider 2 | /+ libstrand=strand 3 | =, strand=strand:libstrand 4 | =, strand-fail=strand-fail:libstrand 5 | |% 6 | ++ send-raw-cards 7 | |= cards=(list =card:agent:gall) 8 | =/ m (strand ,~) 9 | ^- form:m 10 | |= strand-input:strand 11 | [cards %done ~] 12 | :: 13 | ++ send-raw-card 14 | |= =card:agent:gall 15 | =/ m (strand ,~) 16 | ^- form:m 17 | (send-raw-cards card ~) 18 | :: 19 | ++ ignore 20 | |= tin=strand-input:strand 21 | `[%fail %ignore ~] 22 | :: 23 | ++ get-bowl 24 | =/ m (strand ,bowl:strand) 25 | ^- form:m 26 | |= tin=strand-input:strand 27 | `[%done bowl.tin] 28 | :: 29 | ++ get-beak 30 | =/ m (strand ,beak) 31 | ^- form:m 32 | |= tin=strand-input:strand 33 | `[%done [our q.byk da+now]:bowl.tin] 34 | :: 35 | ++ get-time 36 | =/ m (strand ,@da) 37 | ^- form:m 38 | |= tin=strand-input:strand 39 | `[%done now.bowl.tin] 40 | :: 41 | ++ get-our 42 | =/ m (strand ,ship) 43 | ^- form:m 44 | |= tin=strand-input:strand 45 | `[%done our.bowl.tin] 46 | :: 47 | ++ get-entropy 48 | =/ m (strand ,@uvJ) 49 | ^- form:m 50 | |= tin=strand-input:strand 51 | `[%done eny.bowl.tin] 52 | :: 53 | :: Convert skips to %ignore failures. 54 | :: 55 | :: This tells the main loop to try the next handler. 56 | :: 57 | ++ handle 58 | |* a=mold 59 | =/ m (strand ,a) 60 | |= =form:m 61 | ^- form:m 62 | |= tin=strand-input:strand 63 | =/ res (form tin) 64 | =? next.res ?=(%skip -.next.res) 65 | [%fail %ignore ~] 66 | res 67 | :: 68 | :: Wait for a poke with a particular mark 69 | :: 70 | ++ take-poke 71 | |= =mark 72 | =/ m (strand ,vase) 73 | ^- form:m 74 | |= tin=strand-input:strand 75 | ?+ in.tin `[%skip ~] 76 | ~ 77 | `[%wait ~] 78 | :: 79 | [~ %poke @ *] 80 | ?. =(mark p.cage.u.in.tin) 81 | `[%skip ~] 82 | `[%done q.cage.u.in.tin] 83 | == 84 | :: 85 | ++ take-sign-arvo 86 | =/ m (strand ,[wire sign-arvo]) 87 | ^- form:m 88 | |= tin=strand-input:strand 89 | ?+ in.tin `[%skip ~] 90 | ~ 91 | `[%wait ~] 92 | :: 93 | [~ %sign *] 94 | `[%done [wire sign-arvo]:u.in.tin] 95 | == 96 | :: 97 | :: Wait for a subscription update on a wire 98 | :: 99 | ++ take-fact-prefix 100 | |= =wire 101 | =/ m (strand ,[path cage]) 102 | ^- form:m 103 | |= tin=strand-input:strand 104 | ?+ in.tin `[%skip ~] 105 | ~ `[%wait ~] 106 | [~ %agent * %fact *] 107 | ?. =(watch+wire (scag +((lent wire)) wire.u.in.tin)) 108 | `[%skip ~] 109 | `[%done (slag (lent wire) wire.u.in.tin) cage.sign.u.in.tin] 110 | == 111 | :: 112 | :: Wait for a subscription update on a wire 113 | :: 114 | ++ take-fact 115 | |= =wire 116 | =/ m (strand ,cage) 117 | ^- form:m 118 | |= tin=strand-input:strand 119 | ?+ in.tin `[%skip ~] 120 | ~ `[%wait ~] 121 | [~ %agent * %fact *] 122 | ?. =(watch+wire wire.u.in.tin) 123 | `[%skip ~] 124 | `[%done cage.sign.u.in.tin] 125 | == 126 | :: 127 | :: Wait for a subscription close 128 | :: 129 | ++ take-kick 130 | |= =wire 131 | =/ m (strand ,~) 132 | ^- form:m 133 | |= tin=strand-input:strand 134 | ?+ in.tin `[%skip ~] 135 | ~ `[%wait ~] 136 | [~ %agent * %kick *] 137 | ?. =(watch+wire wire.u.in.tin) 138 | `[%skip ~] 139 | `[%done ~] 140 | == 141 | :: 142 | ++ echo 143 | =/ m (strand ,~) 144 | ^- form:m 145 | %- (main-loop ,~) 146 | :~ |= ~ 147 | ^- form:m 148 | ;< =vase bind:m ((handle ,vase) (take-poke %echo)) 149 | =/ message=tape !<(tape vase) 150 | %- (slog leaf+"{message}..." ~) 151 | ;< ~ bind:m (sleep ~s2) 152 | %- (slog leaf+"{message}.." ~) 153 | (pure:m ~) 154 | :: 155 | |= ~ 156 | ^- form:m 157 | ;< =vase bind:m ((handle ,vase) (take-poke %over)) 158 | %- (slog leaf+"over..." ~) 159 | (pure:m ~) 160 | == 161 | :: 162 | ++ take-watch 163 | =/ m (strand ,path) 164 | |= tin=strand-input:strand 165 | ?+ in.tin `[%skip ~] 166 | ~ `[%wait ~] 167 | [~ %watch *] 168 | `[%done path.u.in.tin] 169 | == 170 | :: 171 | ++ take-wake 172 | |= until=(unit @da) 173 | =/ m (strand ,~) 174 | ^- form:m 175 | |= tin=strand-input:strand 176 | ?+ in.tin `[%skip ~] 177 | ~ `[%wait ~] 178 | [~ %sign [%wait @ ~] %behn %wake *] 179 | ?. |(?=(~ until) =(`u.until (slaw %da i.t.wire.u.in.tin))) 180 | `[%skip ~] 181 | ?~ error.sign-arvo.u.in.tin 182 | `[%done ~] 183 | `[%fail %timer-error u.error.sign-arvo.u.in.tin] 184 | == 185 | :: 186 | ++ take-tune 187 | |= =wire 188 | =/ m (strand ,[spar:ames (unit roar:ames)]) 189 | ^- form:m 190 | |= tin=strand-input:strand 191 | ?+ in.tin `[%skip ~] 192 | ~ `[%wait ~] 193 | :: 194 | [~ %sign * %ames %tune ^ *] 195 | ?. =(wire wire.u.in.tin) 196 | `[%skip ~] 197 | `[%done +>.sign-arvo.u.in.tin] 198 | == 199 | :: 200 | ++ take-poke-ack 201 | |= =wire 202 | =/ m (strand ,~) 203 | ^- form:m 204 | |= tin=strand-input:strand 205 | ?+ in.tin `[%skip ~] 206 | ~ `[%wait ~] 207 | [~ %agent * %poke-ack *] 208 | ?. =(wire wire.u.in.tin) 209 | `[%skip ~] 210 | ?~ p.sign.u.in.tin 211 | `[%done ~] 212 | `[%fail %poke-fail u.p.sign.u.in.tin] 213 | == 214 | :: 215 | ++ take-watch-ack 216 | |= =wire 217 | =/ m (strand ,~) 218 | ^- form:m 219 | |= tin=strand-input:strand 220 | ?+ in.tin `[%skip ~] 221 | ~ `[%wait ~] 222 | [~ %agent * %watch-ack *] 223 | ?. =(watch+wire wire.u.in.tin) 224 | `[%skip ~] 225 | ?~ p.sign.u.in.tin 226 | `[%done ~] 227 | `[%fail %watch-ack-fail u.p.sign.u.in.tin] 228 | == 229 | :: 230 | ++ poke 231 | |= [=dock =cage] 232 | =/ m (strand ,~) 233 | ^- form:m 234 | =/ =card:agent:gall [%pass /poke %agent dock %poke cage] 235 | ;< ~ bind:m (send-raw-card card) 236 | (take-poke-ack /poke) 237 | :: 238 | ++ raw-poke 239 | |= [=dock =cage] 240 | =/ m (strand ,~) 241 | ^- form:m 242 | =/ =card:agent:gall [%pass /poke %agent dock %poke cage] 243 | ;< ~ bind:m (send-raw-card card) 244 | =/ m (strand ,~) 245 | ^- form:m 246 | |= tin=strand-input:strand 247 | ?+ in.tin `[%skip ~] 248 | ~ 249 | `[%wait ~] 250 | :: 251 | [~ %agent * %poke-ack *] 252 | ?. =(/poke wire.u.in.tin) 253 | `[%skip ~] 254 | `[%done ~] 255 | == 256 | :: 257 | ++ raw-poke-our 258 | |= [app=term =cage] 259 | =/ m (strand ,~) 260 | ^- form:m 261 | ;< =bowl:spider bind:m get-bowl 262 | (raw-poke [our.bowl app] cage) 263 | :: 264 | ++ poke-our 265 | |= [=term =cage] 266 | =/ m (strand ,~) 267 | ^- form:m 268 | ;< our=@p bind:m get-our 269 | (poke [our term] cage) 270 | :: 271 | ++ watch 272 | |= [=wire =dock =path] 273 | =/ m (strand ,~) 274 | ^- form:m 275 | =/ =card:agent:gall [%pass watch+wire %agent dock %watch path] 276 | ;< ~ bind:m (send-raw-card card) 277 | (take-watch-ack wire) 278 | :: 279 | ++ watch-one 280 | |= [=wire =dock =path] 281 | =/ m (strand ,cage) 282 | ^- form:m 283 | ;< ~ bind:m (watch wire dock path) 284 | ;< =cage bind:m (take-fact wire) 285 | ;< ~ bind:m (take-kick wire) 286 | (pure:m cage) 287 | :: 288 | ++ watch-our 289 | |= [=wire =term =path] 290 | =/ m (strand ,~) 291 | ^- form:m 292 | ;< our=@p bind:m get-our 293 | (watch wire [our term] path) 294 | :: 295 | ++ scry 296 | |* [=mold =path] 297 | =/ m (strand ,mold) 298 | ^- form:m 299 | ?> ?=(^ path) 300 | ?> ?=(^ t.path) 301 | ;< =bowl:spider bind:m get-bowl 302 | %- pure:m 303 | .^(mold i.path (scot %p our.bowl) i.t.path (scot %da now.bowl) t.t.path) 304 | :: 305 | ++ leave 306 | |= [=wire =dock] 307 | =/ m (strand ,~) 308 | ^- form:m 309 | =/ =card:agent:gall [%pass watch+wire %agent dock %leave ~] 310 | (send-raw-card card) 311 | :: 312 | ++ leave-our 313 | |= [=wire =term] 314 | =/ m (strand ,~) 315 | ^- form:m 316 | ;< our=@p bind:m get-our 317 | (leave wire [our term]) 318 | :: 319 | ++ rewatch 320 | |= [=wire =dock =path] 321 | =/ m (strand ,~) 322 | ;< ~ bind:m ((handle ,~) (take-kick wire)) 323 | ;< ~ bind:m (flog-text "rewatching {} {}") 324 | ;< ~ bind:m (watch wire dock path) 325 | (pure:m ~) 326 | :: 327 | ++ wait 328 | |= until=@da 329 | =/ m (strand ,~) 330 | ^- form:m 331 | ;< ~ bind:m (send-wait until) 332 | (take-wake `until) 333 | :: 334 | ++ keen 335 | |= [=wire =spar:ames] 336 | =/ m (strand ,~) 337 | ^- form:m 338 | (send-raw-card %pass wire %arvo %a %keen spar) 339 | :: 340 | ++ sleep 341 | |= for=@dr 342 | =/ m (strand ,~) 343 | ^- form:m 344 | ;< now=@da bind:m get-time 345 | (wait (add now for)) 346 | :: 347 | ++ send-wait 348 | |= until=@da 349 | =/ m (strand ,~) 350 | ^- form:m 351 | =/ =card:agent:gall 352 | [%pass /wait/(scot %da until) %arvo %b %wait until] 353 | (send-raw-card card) 354 | :: 355 | ++ map-err 356 | |* computation-result=mold 357 | =/ m (strand ,computation-result) 358 | |= [f=$-([term tang] [term tang]) computation=form:m] 359 | ^- form:m 360 | |= tin=strand-input:strand 361 | =* loop $ 362 | =/ c-res (computation tin) 363 | ?: ?=(%cont -.next.c-res) 364 | c-res(self.next ..loop(computation self.next.c-res)) 365 | ?. ?=(%fail -.next.c-res) 366 | c-res 367 | c-res(err.next (f err.next.c-res)) 368 | :: 369 | ++ set-timeout 370 | |* computation-result=mold 371 | =/ m (strand ,computation-result) 372 | |= [time=@dr computation=form:m] 373 | ^- form:m 374 | ;< now=@da bind:m get-time 375 | =/ when (add now time) 376 | =/ =card:agent:gall 377 | [%pass /timeout/(scot %da when) %arvo %b %wait when] 378 | ;< ~ bind:m (send-raw-card card) 379 | |= tin=strand-input:strand 380 | =* loop $ 381 | ?: ?& ?=([~ %sign [%timeout @ ~] %behn %wake *] in.tin) 382 | =((scot %da when) i.t.wire.u.in.tin) 383 | == 384 | `[%fail %timeout ~] 385 | =/ c-res (computation tin) 386 | ?: ?=(%cont -.next.c-res) 387 | c-res(self.next ..loop(computation self.next.c-res)) 388 | ?: ?=(%done -.next.c-res) 389 | =/ =card:agent:gall 390 | [%pass /timeout/(scot %da when) %arvo %b %rest when] 391 | c-res(cards [card cards.c-res]) 392 | c-res 393 | :: 394 | ++ send-request 395 | |= =request:http 396 | =/ m (strand ,~) 397 | ^- form:m 398 | (send-raw-card %pass /request %arvo %i %request request *outbound-config:iris) 399 | :: 400 | ++ send-cancel-request 401 | =/ m (strand ,~) 402 | ^- form:m 403 | (send-raw-card %pass /request %arvo %i %cancel-request ~) 404 | :: 405 | ++ take-client-response 406 | =/ m (strand ,client-response:iris) 407 | ^- form:m 408 | |= tin=strand-input:strand 409 | ?+ in.tin `[%skip ~] 410 | ~ `[%wait ~] 411 | :: 412 | [~ %sign [%request ~] %iris %http-response %cancel *] 413 | ::NOTE iris does not (yet?) retry after cancel, so it means failure 414 | :- ~ 415 | :+ %fail 416 | %http-request-cancelled 417 | ['http request was cancelled by the runtime']~ 418 | :: 419 | [~ %sign [%request ~] %iris %http-response %finished *] 420 | `[%done client-response.sign-arvo.u.in.tin] 421 | == 422 | :: 423 | :: Wait until we get an HTTP response or cancelation and unset contract 424 | :: 425 | ++ take-maybe-sigh 426 | =/ m (strand ,(unit httr:eyre)) 427 | ^- form:m 428 | ;< rep=(unit client-response:iris) bind:m 429 | take-maybe-response 430 | ?~ rep 431 | (pure:m ~) 432 | :: XX s/b impossible 433 | :: 434 | ?. ?=(%finished -.u.rep) 435 | (pure:m ~) 436 | (pure:m (some (to-httr:iris +.u.rep))) 437 | :: 438 | ++ take-maybe-response 439 | =/ m (strand ,(unit client-response:iris)) 440 | ^- form:m 441 | |= tin=strand-input:strand 442 | ?+ in.tin `[%skip ~] 443 | ~ `[%wait ~] 444 | [~ %sign [%request ~] %iris %http-response %cancel *] 445 | `[%done ~] 446 | [~ %sign [%request ~] %iris %http-response %finished *] 447 | `[%done `client-response.sign-arvo.u.in.tin] 448 | == 449 | :: 450 | ++ extract-body 451 | |= =client-response:iris 452 | =/ m (strand ,cord) 453 | ^- form:m 454 | ?> ?=(%finished -.client-response) 455 | %- pure:m 456 | ?~ full-file.client-response '' 457 | q.data.u.full-file.client-response 458 | :: 459 | ++ fetch-cord 460 | |= url=tape 461 | =/ m (strand ,cord) 462 | ^- form:m 463 | =/ =request:http [%'GET' (crip url) ~ ~] 464 | ;< ~ bind:m (send-request request) 465 | ;< =client-response:iris bind:m take-client-response 466 | (extract-body client-response) 467 | :: 468 | ++ fetch-json 469 | |= url=tape 470 | =/ m (strand ,json) 471 | ^- form:m 472 | ;< =cord bind:m (fetch-cord url) 473 | =/ json=(unit json) (de-json:html cord) 474 | ?~ json 475 | (strand-fail %json-parse-error ~) 476 | (pure:m u.json) 477 | :: 478 | ++ hiss-request 479 | |= =hiss:eyre 480 | =/ m (strand ,(unit httr:eyre)) 481 | ^- form:m 482 | ;< ~ bind:m (send-request (hiss-to-request:html hiss)) 483 | take-maybe-sigh 484 | :: 485 | :: +build-file: build the source file at the specified $beam 486 | :: 487 | ++ build-file 488 | |= [[=ship =desk =case] =spur] 489 | =* arg +< 490 | =/ m (strand ,(unit vase)) 491 | ^- form:m 492 | ;< =riot:clay bind:m 493 | (warp ship desk ~ %sing %a case spur) 494 | ?~ riot 495 | (pure:m ~) 496 | ?> =(%vase p.r.u.riot) 497 | (pure:m (some !<(vase q.r.u.riot))) 498 | :: 499 | ++ build-file-hard 500 | |= [[=ship =desk =case] =spur] 501 | =* arg +< 502 | =/ m (strand ,vase) 503 | ^- form:m 504 | ;< =riot:clay 505 | bind:m 506 | (warp ship desk ~ %sing %a case spur) 507 | ?> ?=(^ riot) 508 | ?> ?=(%vase p.r.u.riot) 509 | (pure:m !<(vase q.r.u.riot)) 510 | :: +build-mark: build a mark definition to a $dais 511 | :: 512 | ++ build-mark 513 | |= [[=ship =desk =case] mak=mark] 514 | =* arg +< 515 | =/ m (strand ,dais:clay) 516 | ^- form:m 517 | ;< =riot:clay bind:m 518 | (warp ship desk ~ %sing %b case /[mak]) 519 | ?~ riot 520 | (strand-fail %build-mark >arg< ~) 521 | ?> =(%dais p.r.u.riot) 522 | (pure:m !<(dais:clay q.r.u.riot)) 523 | :: +build-tube: build a mark conversion gate ($tube) 524 | :: 525 | ++ build-tube 526 | |= [[=ship =desk =case] =mars:clay] 527 | =* arg +< 528 | =/ m (strand ,tube:clay) 529 | ^- form:m 530 | ;< =riot:clay bind:m 531 | (warp ship desk ~ %sing %c case /[a.mars]/[b.mars]) 532 | ?~ riot 533 | (strand-fail %build-tube >arg< ~) 534 | ?> =(%tube p.r.u.riot) 535 | (pure:m !<(tube:clay q.r.u.riot)) 536 | :: 537 | :: +build-nave: build a mark definition to a $nave 538 | :: 539 | ++ build-nave 540 | |= [[=ship =desk =case] mak=mark] 541 | =* arg +< 542 | =/ m (strand ,vase) 543 | ^- form:m 544 | ;< =riot:clay bind:m 545 | (warp ship desk ~ %sing %e case /[mak]) 546 | ?~ riot 547 | (strand-fail %build-nave >arg< ~) 548 | ?> =(%nave p.r.u.riot) 549 | (pure:m q.r.u.riot) 550 | :: +build-cast: build a mark conversion gate (static) 551 | :: 552 | ++ build-cast 553 | |= [[=ship =desk =case] =mars:clay] 554 | =* arg +< 555 | =/ m (strand ,vase) 556 | ^- form:m 557 | ;< =riot:clay bind:m 558 | (warp ship desk ~ %sing %f case /[a.mars]/[b.mars]) 559 | ?~ riot 560 | (strand-fail %build-cast >arg< ~) 561 | ?> =(%cast p.r.u.riot) 562 | (pure:m q.r.u.riot) 563 | :: 564 | :: Read from Clay 565 | :: 566 | ++ warp 567 | |= [=ship =riff:clay] 568 | =/ m (strand ,riot:clay) 569 | ;< ~ bind:m (send-raw-card %pass /warp %arvo %c %warp ship riff) 570 | (take-writ /warp) 571 | :: 572 | ++ read-file 573 | |= [[=ship =desk =case] =spur] 574 | =* arg +< 575 | =/ m (strand ,cage) 576 | ;< =riot:clay bind:m (warp ship desk ~ %sing %x case spur) 577 | ?~ riot 578 | (strand-fail %read-file >arg< ~) 579 | (pure:m r.u.riot) 580 | :: 581 | ++ check-for-file 582 | |= [[=ship =desk =case] =spur] 583 | =/ m (strand ,?) 584 | ;< =riot:clay bind:m (warp ship desk ~ %sing %x case spur) 585 | (pure:m ?=(^ riot)) 586 | :: 587 | ++ list-tree 588 | |= [[=ship =desk =case] =spur] 589 | =* arg +< 590 | =/ m (strand ,(list path)) 591 | ;< =riot:clay bind:m (warp ship desk ~ %sing %t case spur) 592 | ?~ riot 593 | (strand-fail %list-tree >arg< ~) 594 | (pure:m !<((list path) q.r.u.riot)) 595 | :: 596 | :: Take Clay read result 597 | :: 598 | ++ take-writ 599 | |= =wire 600 | =/ m (strand ,riot:clay) 601 | ^- form:m 602 | |= tin=strand-input:strand 603 | ?+ in.tin `[%skip ~] 604 | ~ `[%wait ~] 605 | [~ %sign * ?(%behn %clay) %writ *] 606 | ?. =(wire wire.u.in.tin) 607 | `[%skip ~] 608 | `[%done +>.sign-arvo.u.in.tin] 609 | == 610 | :: +check-online: require that peer respond before timeout 611 | :: 612 | ++ check-online 613 | |= [who=ship lag=@dr] 614 | =/ m (strand ,~) 615 | ^- form:m 616 | %+ (map-err ,~) |=(* [%offline *tang]) 617 | %+ (set-timeout ,~) lag 618 | ;< ~ bind:m 619 | (poke [who %hood] %helm-hi !>(~)) 620 | (pure:m ~) 621 | :: 622 | ++ eval-hoon 623 | |= [gen=hoon bez=(list beam)] 624 | =/ m (strand ,vase) 625 | ^- form:m 626 | =/ sut=vase !>(..zuse) 627 | |- 628 | ?~ bez 629 | (pure:m (slap sut gen)) 630 | ;< vax=vase bind:m (build-file-hard i.bez) 631 | $(bez t.bez, sut (slop vax sut)) 632 | :: 633 | ++ send-thread 634 | |= [=bear:khan =shed:khan =wire] 635 | =/ m (strand ,~) 636 | ^- form:m 637 | (send-raw-card %pass wire %arvo %k %lard bear shed) 638 | :: 639 | :: Queue on skip, try next on fail %ignore 640 | :: 641 | ++ main-loop 642 | |* a=mold 643 | =/ m (strand ,~) 644 | =/ m-a (strand ,a) 645 | =| queue=(qeu (unit input:strand)) 646 | =| active=(unit [in=(unit input:strand) =form:m-a forms=(list $-(a form:m-a))]) 647 | =| state=a 648 | |= forms=(lest $-(a form:m-a)) 649 | ^- form:m 650 | |= tin=strand-input:strand 651 | =* top `form:m`..$ 652 | =. queue (~(put to queue) in.tin) 653 | |^ (continue bowl.tin) 654 | :: 655 | ++ continue 656 | |= =bowl:strand 657 | ^- output:m 658 | ?> =(~ active) 659 | ?: =(~ queue) 660 | `[%cont top] 661 | =^ in=(unit input:strand) queue ~(get to queue) 662 | ^- output:m 663 | =. active `[in (i.forms state) t.forms] 664 | ^- output:m 665 | (run bowl in) 666 | :: 667 | ++ run 668 | ^- form:m 669 | |= tin=strand-input:strand 670 | ^- output:m 671 | ?> ?=(^ active) 672 | =/ res (form.u.active tin) 673 | =/ =output:m 674 | ?- -.next.res 675 | %wait `[%wait ~] 676 | %skip `[%cont ..$(queue (~(put to queue) in.tin))] 677 | %cont `[%cont ..$(active `[in.u.active self.next.res forms.u.active])] 678 | %done (continue(active ~, state value.next.res) bowl.tin) 679 | %fail 680 | ?: &(?=(^ forms.u.active) ?=(%ignore p.err.next.res)) 681 | %= $ 682 | active `[in.u.active (i.forms.u.active state) t.forms.u.active] 683 | in.tin in.u.active 684 | == 685 | `[%fail err.next.res] 686 | == 687 | [(weld cards.res cards.output) next.output] 688 | -- 689 | :: 690 | ++ retry 691 | |* result=mold 692 | |= [crash-after=(unit @ud) computation=_*form:(strand (unit result))] 693 | =/ m (strand ,result) 694 | =| try=@ud 695 | |- ^- form:m 696 | =* loop $ 697 | ?: =(crash-after `try) 698 | (strand-fail %retry-too-many ~) 699 | ;< ~ bind:m (backoff try ~m1) 700 | ;< res=(unit result) bind:m computation 701 | ?^ res 702 | (pure:m u.res) 703 | loop(try +(try)) 704 | :: 705 | ++ backoff 706 | |= [try=@ud limit=@dr] 707 | =/ m (strand ,~) 708 | ^- form:m 709 | ;< eny=@uvJ bind:m get-entropy 710 | %- sleep 711 | %+ min limit 712 | ?: =(0 try) ~s0 713 | %+ add 714 | (mul ~s1 (bex (dec try))) 715 | (mul ~s0..0001 (~(rad og eny) 1.000)) 716 | :: 717 | :: ---- 718 | :: 719 | :: Output 720 | :: 721 | ++ flog 722 | |= =flog:dill 723 | =/ m (strand ,~) 724 | ^- form:m 725 | (send-raw-card %pass / %arvo %d %flog flog) 726 | :: 727 | ++ flog-text 728 | |= =tape 729 | =/ m (strand ,~) 730 | ^- form:m 731 | (flog %text tape) 732 | :: 733 | ++ flog-tang 734 | |= =tang 735 | =/ m (strand ,~) 736 | ^- form:m 737 | =/ =wall 738 | (zing (turn (flop tang) (cury wash [0 80]))) 739 | |- ^- form:m 740 | =* loop $ 741 | ?~ wall 742 | (pure:m ~) 743 | ;< ~ bind:m (flog-text i.wall) 744 | loop(wall t.wall) 745 | :: 746 | ++ trace 747 | |= =tang 748 | =/ m (strand ,~) 749 | ^- form:m 750 | (pure:m ((slog tang) ~)) 751 | :: 752 | ++ app-message 753 | |= [app=term =cord =tang] 754 | =/ m (strand ,~) 755 | ^- form:m 756 | =/ msg=tape :(weld (trip app) ": " (trip cord)) 757 | ;< ~ bind:m (flog-text msg) 758 | (flog-tang tang) 759 | :: 760 | :: ---- 761 | :: 762 | :: Handle domains 763 | :: 764 | ++ install-domain 765 | |= =turf 766 | =/ m (strand ,~) 767 | ^- form:m 768 | (send-raw-card %pass / %arvo %e %rule %turf %put turf) 769 | :: 770 | :: ---- 771 | :: 772 | :: Threads 773 | :: 774 | ++ start-thread 775 | |= file=term 776 | =/ m (strand ,tid:spider) 777 | ;< =bowl:spider bind:m get-bowl 778 | (start-thread-with-args byk.bowl file *vase) 779 | :: 780 | ++ start-thread-with-args 781 | |= [=beak file=term args=vase] 782 | =/ m (strand ,tid:spider) 783 | ^- form:m 784 | ;< =bowl:spider bind:m get-bowl 785 | =/ tid 786 | (scot %ta (cat 3 (cat 3 'strand_' file) (scot %uv (sham file eny.bowl)))) 787 | =/ poke-vase !>(`start-args:spider`[`tid.bowl `tid beak file args]) 788 | ;< ~ bind:m (poke-our %spider %spider-start poke-vase) 789 | ;< ~ bind:m (sleep ~s0) :: wait for thread to start 790 | (pure:m tid) 791 | :: 792 | +$ thread-result 793 | (each vase [term tang]) 794 | :: 795 | ++ await-thread 796 | |= [file=term args=vase] 797 | =/ m (strand ,thread-result) 798 | ^- form:m 799 | ;< =bowl:spider bind:m get-bowl 800 | =/ tid (scot %ta (cat 3 'strand_' (scot %uv (sham file eny.bowl)))) 801 | =/ poke-vase !>(`start-args:spider`[`tid.bowl `tid byk.bowl file args]) 802 | ;< ~ bind:m (watch-our /awaiting/[tid] %spider /thread-result/[tid]) 803 | ;< ~ bind:m (poke-our %spider %spider-start poke-vase) 804 | ;< ~ bind:m (sleep ~s0) :: wait for thread to start 805 | ;< =cage bind:m (take-fact /awaiting/[tid]) 806 | ;< ~ bind:m (take-kick /awaiting/[tid]) 807 | ?+ p.cage ~|([%strange-thread-result p.cage file tid] !!) 808 | %thread-done (pure:m %& q.cage) 809 | %thread-fail (pure:m %| ;;([term tang] q.q.cage)) 810 | == 811 | -- 812 | -------------------------------------------------------------------------------- /desk/lib/tomelib.hoon: -------------------------------------------------------------------------------- 1 | /- *tome 2 | |% 3 | ++ dejs =, dejs:format 4 | |% 5 | ++ kv-action 6 | |= jon=json 7 | ^- ^kv-action 8 | =* perl (su (perk [%our %space %open %unset %yes %no ~])) 9 | =* invl (su (perk [%read %write %admin %block ~])) 10 | %. jon 11 | %- of 12 | :~ set-value/(ot ~[ship/so space/so app/so bucket/so key/so value/so]) 13 | remove-value/(ot ~[ship/so space/so app/so bucket/so key/so]) 14 | clear-kv/(ot ~[ship/so space/so app/so bucket/so]) 15 | verify-kv/(ot ~[ship/so space/so app/so bucket/so]) 16 | watch-kv/(ot ~[ship/so space/so app/so bucket/so]) 17 | team-kv/(ot ~[ship/so space/so app/so bucket/so]) 18 | perm-kv/(ot ~[ship/so space/so app/so bucket/so perm/(ot ~[read/perl write/perl admin/perl])]) 19 | invite-kv/(ot ~[ship/so space/so app/so bucket/so guy/so level/invl]) 20 | == 21 | :: 22 | ++ feed-action 23 | |= jon=json 24 | ^- ^feed-action 25 | =* perl (su (perk [%our %space %open %unset %yes %no ~])) 26 | =* invl (su (perk [%read %write %admin %block ~])) 27 | %. jon 28 | %- of 29 | :~ new-post/(ot ~[ship/so space/so app/so bucket/so log/bo id/so content/so]) 30 | edit-post/(ot ~[ship/so space/so app/so bucket/so log/bo id/so content/so]) 31 | delete-post/(ot ~[ship/so space/so app/so bucket/so log/bo id/so]) 32 | clear-feed/(ot ~[ship/so space/so app/so bucket/so log/bo]) 33 | verify-feed/(ot ~[ship/so space/so app/so bucket/so log/bo]) 34 | watch-feed/(ot ~[ship/so space/so app/so bucket/so log/bo]) 35 | team-feed/(ot ~[ship/so space/so app/so bucket/so log/bo]) 36 | perm-feed/(ot ~[ship/so space/so app/so bucket/so log/bo perm/(ot ~[read/perl write/perl admin/perl])]) 37 | invite-feed/(ot ~[ship/so space/so app/so bucket/so log/bo guy/so level/invl]) 38 | set-post-link/(ot ~[ship/so space/so app/so bucket/so log/bo id/so value/so]) 39 | remove-post-link/(ot ~[ship/so space/so app/so bucket/so log/bo id/so]) 40 | == 41 | -- 42 | ++ enjs =, enjs:format 43 | |% 44 | ++ kv-update 45 | |= upd=^kv-update 46 | ^- json 47 | ?- -.upd 48 | %set (frond key.upd s+value.upd) 49 | %remove (frond key.upd ~) 50 | %clear (pairs ~) 51 | %perm (pairs ~[[%write s+write.upd] [%admin s+admin.upd]]) 52 | %get value.upd 53 | %all o+data.upd 54 | == 55 | :: 56 | ++ feed-convert 57 | |= x=[k=@da v=feed-value] 58 | ^- json 59 | %- pairs 60 | :~ [%id s+id.v.x] 61 | [%'createdBy' s+(scot %p created-by.v.x)] 62 | [%'updatedBy' s+(scot %p updated-by.v.x)] 63 | [%'createdAt' (time created-at.v.x)] 64 | [%'updatedAt' (time updated-at.v.x)] 65 | [%content content.v.x] 66 | [%links o+links.v.x] 67 | == 68 | :: 69 | ++ feed-update 70 | |= upd=^feed-update 71 | ^- json 72 | ?- -.upd 73 | %new (pairs ~[[%type s+'new'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+(scot %p ship.upd)] [%content s+content.upd]])]]) 74 | %edit (pairs ~[[%type s+'edit'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+(scot %p ship.upd)] [%content s+content.upd]])]]) :: time is updated-time, ship is updated-by 75 | %delete (pairs ~[[%type s+'delete'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)]])]]) 76 | %clear (frond %type s+'clear') 77 | %set-link (pairs ~[[%type s+'set-link'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+ship.upd] [%value s+value.upd]])]]) 78 | %remove-link (pairs ~[[%type s+'remove-link'] [%body (pairs ~[[%id s+id.upd] [%time (time time.upd)] [%ship s+ship.upd]])]]) 79 | %perm (pairs ~[[%write s+write.upd] [%admin s+admin.upd]]) 80 | %get 81 | ?~ post.upd ~ 82 | (feed-convert [*@da post.upd]) 83 | %all 84 | =/ fon ((on @da feed-value) gth) :: mop needs this to work 85 | =/ data-list (tap:fon data.upd) 86 | :- %a 87 | (turn data-list feed-convert) 88 | == 89 | -- 90 | -- -------------------------------------------------------------------------------- /desk/lib/verb.hoon: -------------------------------------------------------------------------------- 1 | :: Print what your agent is doing. 2 | :: 3 | /- verb 4 | :: 5 | |= [loud=? =agent:gall] 6 | =| bowl-print=_| 7 | ^- agent:gall 8 | |^ !. 9 | |_ =bowl:gall 10 | +* this . 11 | ag ~(. agent bowl) 12 | :: 13 | ++ on-init 14 | ^- (quip card:agent:gall agent:gall) 15 | %- (print bowl |.("{}: on-init")) 16 | =^ cards agent on-init:ag 17 | [[(emit-event %on-init ~) cards] this] 18 | :: 19 | ++ on-save 20 | ^- vase 21 | %- (print bowl |.("{}: on-save")) 22 | on-save:ag 23 | :: 24 | ++ on-load 25 | |= old-state=vase 26 | ^- (quip card:agent:gall agent:gall) 27 | %- (print bowl |.("{}: on-load")) 28 | =^ cards agent (on-load:ag old-state) 29 | [[(emit-event %on-load ~) cards] this] 30 | :: 31 | ++ on-poke 32 | |= [=mark =vase] 33 | ^- (quip card:agent:gall agent:gall) 34 | %- (print bowl |.("{}: on-poke with mark {}")) 35 | ?: ?=(%verb mark) 36 | ?- !<(?(%loud %bowl) vase) 37 | %loud `this(loud !loud) 38 | %bowl `this(bowl-print !bowl-print) 39 | == 40 | =^ cards agent (on-poke:ag mark vase) 41 | [[(emit-event %on-poke mark) cards] this] 42 | :: 43 | ++ on-watch 44 | |= =path 45 | ^- (quip card:agent:gall agent:gall) 46 | %- (print bowl |.("{}: on-watch on path {}")) 47 | =^ cards agent 48 | ?: ?=([%verb %events ~] path) 49 | [~ agent] 50 | (on-watch:ag path) 51 | [[(emit-event %on-watch path) cards] this] 52 | :: 53 | ++ on-leave 54 | |= =path 55 | ^- (quip card:agent:gall agent:gall) 56 | %- (print bowl |.("{}: on-leave on path {}")) 57 | ?: ?=([%verb %event ~] path) 58 | [~ this] 59 | =^ cards agent (on-leave:ag path) 60 | [[(emit-event %on-leave path) cards] this] 61 | :: 62 | ++ on-peek 63 | |= =path 64 | ^- (unit (unit cage)) 65 | %- (print bowl |.("{}: on-peek on path {}")) 66 | (on-peek:ag path) 67 | :: 68 | ++ on-agent 69 | |= [=wire =sign:agent:gall] 70 | ^- (quip card:agent:gall agent:gall) 71 | %- (print bowl |.("{}: on-agent on wire {}, {<-.sign>}")) 72 | =^ cards agent (on-agent:ag wire sign) 73 | [[(emit-event %on-agent wire -.sign) cards] this] 74 | :: 75 | ++ on-arvo 76 | |= [=wire =sign-arvo] 77 | ^- (quip card:agent:gall agent:gall) 78 | %- %+ print bowl |. 79 | "{}: on-arvo on wire {}, {<[- +<]:sign-arvo>}" 80 | =^ cards agent (on-arvo:ag wire sign-arvo) 81 | [[(emit-event %on-arvo wire [- +<]:sign-arvo) cards] this] 82 | :: 83 | ++ on-fail 84 | |= [=term =tang] 85 | ^- (quip card:agent:gall agent:gall) 86 | %- (print bowl |.("{}: on-fail with term {}")) 87 | =^ cards agent (on-fail:ag term tang) 88 | [[(emit-event %on-fail term) cards] this] 89 | -- 90 | :: 91 | ++ print 92 | |= [=bowl:gall render=(trap tape)] 93 | ^+ same 94 | =? . bowl-print 95 | %- (slog >bowl< ~) 96 | . 97 | ?. loud same 98 | %- (slog [%leaf $:render] ~) 99 | same 100 | :: 101 | ++ emit-event 102 | |= =event:verb 103 | ^- card:agent:gall 104 | [%give %fact ~[/verb/events] %verb-event !>(event)] 105 | -- 106 | -------------------------------------------------------------------------------- /desk/mar/bill.hoon: -------------------------------------------------------------------------------- 1 | |_ bil=(list dude:gall) 2 | ++ grow 3 | |% 4 | ++ mime `^mime`[/text/x-bill (as-octs:mimes:html hoon)] 5 | ++ noun bil 6 | ++ hoon 7 | ^- @t 8 | |^ (crip (of-wall:format (wrap-lines (spit-duz bil)))) 9 | :: 10 | ++ wrap-lines 11 | |= taz=wall 12 | ^- wall 13 | ?~ taz ["~"]~ 14 | :- (weld ":~ " i.taz) 15 | %- snoc :_ "==" 16 | (turn t.taz |=(t=tape (weld " " t))) 17 | :: 18 | ++ spit-duz 19 | |= duz=(list dude:gall) 20 | ^- wall 21 | (turn duz |=(=dude:gall ['%' (trip dude)])) 22 | -- 23 | ++ txt (to-wain:format hoon) 24 | -- 25 | ++ grab 26 | |% 27 | ++ noun (list dude:gall) 28 | ++ mime 29 | |= [=mite len=@ud tex=@] 30 | ~_ tex 31 | !<((list dude:gall) (slap !>(~) (ream tex))) 32 | -- 33 | ++ grad %noun 34 | -- 35 | -------------------------------------------------------------------------------- /desk/mar/docket-0.hoon: -------------------------------------------------------------------------------- 1 | /+ dock=docket 2 | |_ =docket:dock 3 | ++ grow 4 | |% 5 | ++ mime 6 | ^- ^mime 7 | [/text/x-docket (as-octt:mimes:html (spit-docket:mime:dock docket))] 8 | ++ noun docket 9 | ++ json (docket:enjs:dock docket) 10 | -- 11 | ++ grab 12 | |% 13 | :: 14 | ++ mime 15 | |= [=mite len=@ud tex=@] 16 | ^- docket:dock 17 | %- need 18 | %- from-clauses:mime:dock 19 | !<((list clause:dock) (slap !>(~) (ream tex))) 20 | 21 | :: 22 | ++ noun docket:dock 23 | -- 24 | ++ grad %noun 25 | -- 26 | -------------------------------------------------------------------------------- /desk/mar/feed/action.hoon: -------------------------------------------------------------------------------- 1 | /- *tome 2 | /+ tomelib 3 | |_ act=feed-action 4 | ++ grow 5 | |% 6 | ++ noun act 7 | -- 8 | ++ grab 9 | |% 10 | ++ noun feed-action 11 | ++ json feed-action:dejs:tomelib 12 | -- 13 | ++ grad %noun 14 | -- -------------------------------------------------------------------------------- /desk/mar/feed/update.hoon: -------------------------------------------------------------------------------- 1 | /- *tome 2 | /+ tomelib 3 | |_ upd=feed-update 4 | ++ grab 5 | |% 6 | ++ noun feed-update 7 | -- 8 | ++ grow 9 | |% 10 | ++ noun upd 11 | ++ json (feed-update:enjs:tomelib upd) 12 | -- 13 | ++ grad %noun 14 | -- -------------------------------------------------------------------------------- /desk/mar/hoon.hoon: -------------------------------------------------------------------------------- 1 | :::: /hoon/hoon/mar 2 | :: 3 | /? 310 4 | :: 5 | =, eyre 6 | |_ own=@t 7 | :: 8 | ++ grow :: convert to 9 | |% 10 | ++ mime `^mime`[/text/x-hoon (as-octs:mimes:html own)] :: convert to %mime 11 | ++ elem :: convert to %html 12 | ;div:pre(urb_codemirror "", mode "hoon"):"{(trip own)}" 13 | :: =+ gen-id="src-{<`@ui`(mug own)>}" 14 | :: ;div 15 | :: ;textarea(id "{gen-id}"):"{(trip own)}" 16 | :: ;script:""" 17 | :: CodeMirror.fromTextArea( 18 | :: window[{}], 19 | :: \{lineNumbers:true, readOnly:true} 20 | :: ) 21 | :: """ 22 | :: == 23 | ++ hymn 24 | :: ;html:(head:title:"Source" "+{elem}") 25 | ;html 26 | ;head 27 | ;title:"Source" 28 | ;script@"//cdnjs.cloudflare.com/ajax/libs/codemirror/4.3.0/codemirror.js"; 29 | ;script@"/lib/syntax/hoon.js"; 30 | ;link(rel "stylesheet", href "//cdnjs.cloudflare.com/ajax/libs/". 31 | "codemirror/4.3.0/codemirror.min.css"); 32 | ;link/"/lib/syntax/codemirror.css"(rel "stylesheet"); 33 | == 34 | ;body 35 | ;textarea#src:"{(trip own)}" 36 | ;script:'CodeMirror.fromTextArea(src, {lineNumbers:true, readOnly:true})' 37 | == 38 | == 39 | ++ txt 40 | (to-wain:format own) 41 | -- 42 | ++ grab 43 | |% :: convert from 44 | ++ mime |=([p=mite q=octs] q.q) 45 | ++ noun @t :: clam from %noun 46 | ++ txt of-wain:format 47 | -- 48 | ++ grad %txt 49 | -- 50 | -------------------------------------------------------------------------------- /desk/mar/json.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/json/mar 3 | :: 4 | /? 310 5 | :: 6 | :::: compute 7 | :: 8 | =, eyre 9 | =, format 10 | =, html 11 | |_ jon=^json 12 | :: 13 | ++ grow :: convert to 14 | |% 15 | ++ mime [/application/json (as-octs:mimes -:txt)] :: convert to %mime 16 | ++ txt [(en:json jon)]~ 17 | -- 18 | ++ grab 19 | |% :: convert from 20 | ++ mime |=([p=mite q=octs] (fall (de:json (@t q.q)) *^json)) 21 | ++ noun ^json :: clam from %noun 22 | ++ numb numb:enjs 23 | ++ time time:enjs 24 | -- 25 | ++ grad %mime 26 | -- 27 | -------------------------------------------------------------------------------- /desk/mar/kelvin.hoon: -------------------------------------------------------------------------------- 1 | |_ kal=waft:clay 2 | ++ grow 3 | |% 4 | ++ mime `^mime`[/text/x-kelvin (as-octs:mimes:html hoon)] 5 | ++ noun kal 6 | ++ hoon 7 | %+ rap 3 8 | %+ turn 9 | %+ sort 10 | ~(tap in (waft-to-wefts:clay kal)) 11 | |= [a=weft b=weft] 12 | ?: =(lal.a lal.b) 13 | (gte num.a num.b) 14 | (gte lal.a lal.b) 15 | |= =weft 16 | (rap 3 '[%' (scot %tas lal.weft) ' ' (scot %ud num.weft) ']\0a' ~) 17 | :: 18 | ++ txt (to-wain:format hoon) 19 | -- 20 | ++ grab 21 | |% 22 | ++ noun waft:clay 23 | ++ mime 24 | |= [=mite len=@ud tex=@] 25 | (cord-to-waft:clay tex) 26 | -- 27 | ++ grad %noun 28 | -- 29 | -------------------------------------------------------------------------------- /desk/mar/kv/action.hoon: -------------------------------------------------------------------------------- 1 | /- *tome 2 | /+ tomelib 3 | |_ act=kv-action 4 | ++ grow 5 | |% 6 | ++ noun act 7 | -- 8 | ++ grab 9 | |% 10 | ++ noun kv-action 11 | ++ json kv-action:dejs:tomelib 12 | -- 13 | ++ grad %noun 14 | -- -------------------------------------------------------------------------------- /desk/mar/kv/update.hoon: -------------------------------------------------------------------------------- 1 | /- *tome 2 | /+ tomelib 3 | |_ upd=kv-update 4 | ++ grab 5 | |% 6 | ++ noun kv-update 7 | -- 8 | ++ grow 9 | |% 10 | ++ noun upd 11 | ++ json (kv-update:enjs:tomelib upd) 12 | -- 13 | ++ grad %noun 14 | -- -------------------------------------------------------------------------------- /desk/mar/mime.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/mime/mar 3 | :: 4 | /? 310 5 | :: 6 | |_ own=mime 7 | ++ grow 8 | ^? 9 | |% 10 | ++ jam `@`q.q.own 11 | -- 12 | :: 13 | ++ grab :: convert from 14 | ^? 15 | |% 16 | ++ noun mime :: clam from %noun 17 | ++ tape 18 | |=(a=_"" [/application/x-urb-unknown (as-octt:mimes:html a)]) 19 | -- 20 | ++ grad 21 | ^? 22 | |% 23 | ++ form %mime 24 | ++ diff |=(mime +<) 25 | ++ pact |=(mime +<) 26 | ++ join |=([mime mime] `(unit mime)`~) 27 | ++ mash 28 | |= [[ship desk mime] [ship desk mime]] 29 | ^- mime 30 | ~|(%mime-mash !!) 31 | -- 32 | -- 33 | -------------------------------------------------------------------------------- /desk/mar/noun.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/noun/mar 3 | :: 4 | /? 310 5 | !: 6 | :::: A minimal noun mark 7 | |_ non=* 8 | ++ grab |% 9 | ++ noun * 10 | -- 11 | ++ grad 12 | |% 13 | ++ form %noun 14 | ++ diff |=(* +<) 15 | ++ pact |=(* +<) 16 | ++ join |=([* *] *(unit *)) 17 | ++ mash |=([[ship desk *] [ship desk *]] `*`~|(%noun-mash !!)) 18 | -- 19 | -- 20 | -------------------------------------------------------------------------------- /desk/mar/ship.hoon: -------------------------------------------------------------------------------- 1 | |_ s=ship 2 | ++ grad %noun 3 | ++ grow 4 | |% 5 | ++ noun s 6 | ++ json s+(scot %p s) 7 | ++ mime 8 | ^- ^mime 9 | [/text/x-ship (as-octt:mimes:html (scow %p s))] 10 | 11 | -- 12 | ++ grab 13 | |% 14 | ++ noun ship 15 | ++ json (su:dejs:format ;~(pfix sig fed:ag)) 16 | ++ mime 17 | |= [=mite len=@ tex=@] 18 | (slav %p (snag 0 (to-wain:format tex))) 19 | -- 20 | -- 21 | -------------------------------------------------------------------------------- /desk/mar/tome/action.hoon: -------------------------------------------------------------------------------- 1 | /- *tome 2 | |_ act=tome-action 3 | ++ grow 4 | |% 5 | ++ noun act 6 | -- 7 | ++ grab 8 | |% 9 | ++ noun tome-action 10 | ++ json 11 | =, dejs:format 12 | |= jon=json 13 | ^- tome-action 14 | =* levels (su (perk [%our %space %open %unset %yes %no ~])) 15 | %. jon 16 | %- of 17 | :~ init-tome/(ot ~[ship/so space/so app/so]) 18 | init-kv/(ot ~[ship/so space/so app/so bucket/so perm/(ot ~[read/levels write/levels admin/levels])]) 19 | init-feed/(ot ~[ship/so space/so app/so bucket/so log/bo perm/(ot ~[read/levels write/levels admin/levels])]) 20 | == 21 | -- 22 | ++ grad %noun 23 | -- -------------------------------------------------------------------------------- /desk/mar/txt.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/txt/mar 3 | :: 4 | /? 310 5 | :: 6 | =, clay 7 | =, differ 8 | =, format 9 | =, mimes:html 10 | |_ txt=wain 11 | :: 12 | ++ grab :: convert from 13 | |% 14 | ++ mime |=((pair mite octs) (to-wain q.q)) 15 | ++ noun wain :: clam from %noun 16 | -- 17 | ++ grow 18 | => v=. 19 | |% 20 | ++ mime => v [/text/plain (as-octs (of-wain txt))] 21 | ++ elem => v ;pre: {(trip (of-wain txt))} 22 | -- 23 | ++ grad 24 | |% 25 | ++ form %txt-diff 26 | ++ diff 27 | |= tyt=wain 28 | ^- (urge cord) 29 | (lusk txt tyt (loss txt tyt)) 30 | :: 31 | ++ pact 32 | |= dif=(urge cord) 33 | ~| [%pacting dif] 34 | ^- wain 35 | (lurk txt dif) 36 | :: 37 | ++ join 38 | |= [ali=(urge cord) bob=(urge cord)] 39 | ^- (unit (urge cord)) 40 | |^ 41 | =. ali (clean ali) 42 | =. bob (clean bob) 43 | |- ^- (unit (urge cord)) 44 | ?~ ali `bob 45 | ?~ bob `ali 46 | ?- -.i.ali 47 | %& 48 | ?- -.i.bob 49 | %& 50 | ?: =(p.i.ali p.i.bob) 51 | %+ bind $(ali t.ali, bob t.bob) 52 | |=(cud=(urge cord) [i.ali cud]) 53 | ?: (gth p.i.ali p.i.bob) 54 | %+ bind $(p.i.ali (sub p.i.ali p.i.bob), bob t.bob) 55 | |=(cud=(urge cord) [i.bob cud]) 56 | %+ bind $(ali t.ali, p.i.bob (sub p.i.bob p.i.ali)) 57 | |=(cud=(urge cord) [i.ali cud]) 58 | :: 59 | %| 60 | ?: =(p.i.ali (lent p.i.bob)) 61 | %+ bind $(ali t.ali, bob t.bob) 62 | |=(cud=(urge cord) [i.bob cud]) 63 | ?: (gth p.i.ali (lent p.i.bob)) 64 | %+ bind $(p.i.ali (sub p.i.ali (lent p.i.bob)), bob t.bob) 65 | |=(cud=(urge cord) [i.bob cud]) 66 | ~ 67 | == 68 | :: 69 | %| 70 | ?- -.i.bob 71 | %| 72 | ?. =(i.ali i.bob) 73 | ~ 74 | %+ bind $(ali t.ali, bob t.bob) 75 | |=(cud=(urge cord) [i.ali cud]) 76 | :: 77 | %& 78 | ?: =(p.i.bob (lent p.i.ali)) 79 | %+ bind $(ali t.ali, bob t.bob) 80 | |=(cud=(urge cord) [i.ali cud]) 81 | ?: (gth p.i.bob (lent p.i.ali)) 82 | %+ bind $(ali t.ali, p.i.bob (sub p.i.bob (lent p.i.ali))) 83 | |=(cud=(urge cord) [i.ali cud]) 84 | ~ 85 | == 86 | == 87 | ++ clean :: clean 88 | |= wig=(urge cord) 89 | ^- (urge cord) 90 | ?~ wig ~ 91 | ?~ t.wig wig 92 | ?: ?=(%& -.i.wig) 93 | ?: ?=(%& -.i.t.wig) 94 | $(wig [[%& (add p.i.wig p.i.t.wig)] t.t.wig]) 95 | [i.wig $(wig t.wig)] 96 | ?: ?=(%| -.i.t.wig) 97 | $(wig [[%| (welp p.i.wig p.i.t.wig) (welp q.i.wig q.i.t.wig)] t.t.wig]) 98 | [i.wig $(wig t.wig)] 99 | -- 100 | :: 101 | ++ mash 102 | |= $: [als=ship ald=desk ali=(urge cord)] 103 | [bos=ship bod=desk bob=(urge cord)] 104 | == 105 | ^- (urge cord) 106 | |^ 107 | =. ali (clean ali) 108 | =. bob (clean bob) 109 | |- ^- (urge cord) 110 | ?~ ali bob 111 | ?~ bob ali 112 | ?- -.i.ali 113 | %& 114 | ?- -.i.bob 115 | %& 116 | ?: =(p.i.ali p.i.bob) 117 | [i.ali $(ali t.ali, bob t.bob)] 118 | ?: (gth p.i.ali p.i.bob) 119 | [i.bob $(p.i.ali (sub p.i.ali p.i.bob), bob t.bob)] 120 | [i.ali $(ali t.ali, p.i.bob (sub p.i.bob p.i.ali))] 121 | :: 122 | %| 123 | ?: =(p.i.ali (lent p.i.bob)) 124 | [i.bob $(ali t.ali, bob t.bob)] 125 | ?: (gth p.i.ali (lent p.i.bob)) 126 | [i.bob $(p.i.ali (sub p.i.ali (lent p.i.bob)), bob t.bob)] 127 | =/ [fic=(unce cord) ali=(urge cord) bob=(urge cord)] 128 | (resolve ali bob) 129 | [fic $(ali ali, bob bob)] 130 | :: ~ :: here, alice is good for a while, but not for the whole 131 | == :: length of bob's changes 132 | :: 133 | %| 134 | ?- -.i.bob 135 | %| 136 | =/ [fic=(unce cord) ali=(urge cord) bob=(urge cord)] 137 | (resolve ali bob) 138 | [fic $(ali ali, bob bob)] 139 | :: 140 | %& 141 | ?: =(p.i.bob (lent p.i.ali)) 142 | [i.ali $(ali t.ali, bob t.bob)] 143 | ?: (gth p.i.bob (lent p.i.ali)) 144 | [i.ali $(ali t.ali, p.i.bob (sub p.i.bob (lent p.i.ali)))] 145 | =/ [fic=(unce cord) ali=(urge cord) bob=(urge cord)] 146 | (resolve ali bob) 147 | [fic $(ali ali, bob bob)] 148 | == 149 | == 150 | :: 151 | ++ annotate :: annotate conflict 152 | |= $: ali=(list @t) 153 | bob=(list @t) 154 | bas=(list @t) 155 | == 156 | ^- (list @t) 157 | %- zing 158 | ^- (list (list @t)) 159 | %- flop 160 | ^- (list (list @t)) 161 | :- :_ ~ 162 | %^ cat 3 '<<<<<<<<<<<<' 163 | %^ cat 3 ' ' 164 | %^ cat 3 `@t`(scot %p bos) 165 | %^ cat 3 '/' 166 | bod 167 | 168 | :- bob 169 | :- ~['------------'] 170 | :- bas 171 | :- ~['++++++++++++'] 172 | :- ali 173 | :- :_ ~ 174 | %^ cat 3 '>>>>>>>>>>>>' 175 | %^ cat 3 ' ' 176 | %^ cat 3 `@t`(scot %p als) 177 | %^ cat 3 '/' 178 | ald 179 | ~ 180 | :: 181 | ++ clean :: clean 182 | |= wig=(urge cord) 183 | ^- (urge cord) 184 | ?~ wig ~ 185 | ?~ t.wig wig 186 | ?: ?=(%& -.i.wig) 187 | ?: ?=(%& -.i.t.wig) 188 | $(wig [[%& (add p.i.wig p.i.t.wig)] t.t.wig]) 189 | [i.wig $(wig t.wig)] 190 | ?: ?=(%| -.i.t.wig) 191 | $(wig [[%| (welp p.i.wig p.i.t.wig) (welp q.i.wig q.i.t.wig)] t.t.wig]) 192 | [i.wig $(wig t.wig)] 193 | :: 194 | ++ resolve 195 | |= [ali=(urge cord) bob=(urge cord)] 196 | ^- [fic=[%| p=(list cord) q=(list cord)] ali=(urge cord) bob=(urge cord)] 197 | =- [[%| bac (annotate alc boc bac)] ali bob] 198 | |- ^- $: $: bac=(list cord) 199 | alc=(list cord) 200 | boc=(list cord) 201 | == 202 | ali=(urge cord) 203 | bob=(urge cord) 204 | == 205 | ?~ ali [[~ ~ ~] ali bob] 206 | ?~ bob [[~ ~ ~] ali bob] 207 | ?- -.i.ali 208 | %& 209 | ?- -.i.bob 210 | %& [[~ ~ ~] ali bob] :: no conflict 211 | %| 212 | =+ lob=(lent p.i.bob) 213 | ?: =(lob p.i.ali) 214 | [[p.i.bob p.i.bob q.i.bob] t.ali t.bob] 215 | ?: (lth lob p.i.ali) 216 | [[p.i.bob p.i.bob q.i.bob] [[%& (sub p.i.ali lob)] t.ali] t.bob] 217 | =+ wat=(scag (sub lob p.i.ali) p.i.bob) 218 | =+ ^= res 219 | %= $ 220 | ali t.ali 221 | bob [[%| (scag (sub lob p.i.ali) p.i.bob) ~] t.bob] 222 | == 223 | :* :* (welp bac.res wat) 224 | (welp alc.res wat) 225 | (welp boc.res q.i.bob) 226 | == 227 | ali.res 228 | bob.res 229 | == 230 | == 231 | :: 232 | %| 233 | ?- -.i.bob 234 | %& 235 | =+ loa=(lent p.i.ali) 236 | ?: =(loa p.i.bob) 237 | [[p.i.ali q.i.ali p.i.ali] t.ali t.bob] 238 | ?: (lth loa p.i.bob) 239 | [[p.i.ali q.i.ali p.i.ali] t.ali [[%& (sub p.i.bob loa)] t.bob]] 240 | =+ wat=(slag (sub loa p.i.bob) p.i.ali) 241 | =+ ^= res 242 | %= $ 243 | ali [[%| (scag (sub loa p.i.bob) p.i.ali) ~] t.ali] 244 | bob t.bob 245 | == 246 | :* :* (welp bac.res wat) 247 | (welp alc.res q.i.ali) 248 | (welp boc.res wat) 249 | == 250 | ali.res 251 | bob.res 252 | == 253 | :: 254 | %| 255 | =+ loa=(lent p.i.ali) 256 | =+ lob=(lent p.i.bob) 257 | ?: =(loa lob) 258 | [[p.i.ali q.i.ali q.i.bob] t.ali t.bob] 259 | =+ ^= res 260 | ?: (gth loa lob) 261 | $(ali [[%| (scag (sub loa lob) p.i.ali) ~] t.ali], bob t.bob) 262 | ~& [%scagging loa=loa pibob=p.i.bob slag=(scag loa p.i.bob)] 263 | $(ali t.ali, bob [[%| (scag (sub lob loa) p.i.bob) ~] t.bob]) 264 | :* :* (welp bac.res ?:((gth loa lob) p.i.bob p.i.ali)) 265 | (welp alc.res q.i.ali) 266 | (welp boc.res q.i.bob) 267 | == 268 | ali.res 269 | bob.res 270 | == 271 | == 272 | == 273 | -- 274 | -- 275 | -- 276 | -------------------------------------------------------------------------------- /desk/sur/docket.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | :: 3 | +$ version 4 | [major=@ud minor=@ud patch=@ud] 5 | :: 6 | +$ glob (map path mime) 7 | :: 8 | +$ url cord 9 | :: $glob-location: How to retrieve a glob 10 | :: 11 | +$ glob-reference 12 | [hash=@uvH location=glob-location] 13 | :: 14 | +$ glob-location 15 | $% [%http =url] 16 | [%ames =ship] 17 | == 18 | :: $href: Where a tile links to 19 | :: 20 | +$ href 21 | $% [%glob base=term =glob-reference] 22 | [%site =path] 23 | == 24 | :: $chad: State of a docket 25 | :: 26 | +$ chad 27 | $~ [%install ~] 28 | $% :: Done 29 | [%glob =glob] 30 | [%site ~] 31 | :: Waiting 32 | [%install ~] 33 | [%suspend glob=(unit glob)] 34 | :: Error 35 | [%hung err=cord] 36 | == 37 | :: 38 | :: $charge: A realized $docket 39 | :: 40 | +$ charge 41 | $: =docket 42 | =chad 43 | == 44 | :: 45 | :: $clause: A key and value, as part of a docket 46 | :: 47 | :: Only used to parse $docket 48 | :: 49 | +$ clause 50 | $% [%title title=@t] 51 | [%info info=@t] 52 | [%color color=@ux] 53 | [%glob-http url=cord hash=@uvH] 54 | [%glob-ames =ship hash=@uvH] 55 | [%image =url] 56 | [%site =path] 57 | [%base base=term] 58 | [%version =version] 59 | [%website website=url] 60 | [%license license=cord] 61 | == 62 | :: 63 | :: $docket: A description of JS bundles for a desk 64 | :: 65 | +$ docket 66 | $: %1 67 | title=@t 68 | info=@t 69 | color=@ux 70 | =href 71 | image=(unit url) 72 | =version 73 | website=url 74 | license=cord 75 | == 76 | :: 77 | +$ charge-update 78 | $% [%initial initial=(map desk charge)] 79 | [%add-charge =desk =charge] 80 | [%del-charge =desk] 81 | == 82 | -- 83 | -------------------------------------------------------------------------------- /desk/sur/membership.hoon: -------------------------------------------------------------------------------- 1 | /- *spaces-path 2 | |% 3 | :: 4 | +$ role ?(%initiate %member %admin %owner) 5 | +$ roles (set role) 6 | +$ status ?(%invited %joined %host) 7 | +$ alias cord 8 | +$ member 9 | $: =roles 10 | =alias 11 | =status 12 | == 13 | +$ members (map ship member) 14 | +$ membership (map path members) 15 | :: 16 | +$ view 17 | $% [%membership =membership] 18 | [%members =members] 19 | [%member =member] 20 | [%is-member is-member=?] 21 | == 22 | -- -------------------------------------------------------------------------------- /desk/sur/rooms-v2.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :: %rooms-v2: is a primitive for who is currently presence. 3 | :: 4 | :: 5 | /- spaces=spaces-path 6 | |% 7 | +$ rid @t 8 | +$ title cord 9 | +$ capacity @ud 10 | +$ access ?(%public %private) 11 | +$ present (set ship) 12 | +$ whitelist (set ship) 13 | +$ space-path path:spaces 14 | 15 | +$ room 16 | $: =rid 17 | provider=ship 18 | creator=ship 19 | =access 20 | =title 21 | =present 22 | =whitelist 23 | capacity=@ud 24 | path=(unit cord) 25 | == 26 | :: 27 | +$ rooms (map rid room) 28 | :: 29 | +$ session-state 30 | $: provider=ship 31 | current=(unit rid) 32 | =rooms 33 | == 34 | :: 35 | +$ provider-state 36 | $: =rooms 37 | online=? 38 | banned=(set ship) 39 | == 40 | :: 41 | +$ edit-payload 42 | $% [%title =title] 43 | [%access =access] 44 | == 45 | :: 46 | +$ session-action 47 | $% [%set-provider =ship] 48 | [%reset-provider ~] 49 | [%create-room =rid =access =title path=(unit cord)] 50 | [%edit-room =rid =title =access] 51 | [%delete-room =rid] 52 | [%enter-room =rid] 53 | [%leave-room =rid] 54 | [%invite =rid =ship] 55 | [%kick =rid =ship] 56 | [%send-chat content=cord] 57 | == 58 | :: 59 | +$ reaction 60 | $% [%room-entered =rid =ship] 61 | [%room-left =rid =ship] 62 | [%room-created =room] 63 | [%room-updated =room] 64 | [%room-deleted =rid] 65 | [%provider-changed provider=ship =rooms] 66 | [%invited provider=ship =rid =title =ship] 67 | [%kicked =rid =ship] 68 | [%chat-received from=ship content=cord] 69 | == 70 | :: 71 | +$ provider-action 72 | $% [%set-online online=?] 73 | [%ban =ship] 74 | [%unban =ship] 75 | == 76 | :: 77 | +$ signal-action 78 | $% 79 | [%signal from=ship to=ship rid=cord data=cord] 80 | == 81 | :: 82 | +$ view 83 | $% [%session =session-state] 84 | [%room =room] 85 | [%provider =ship] 86 | == 87 | :: 88 | -- 89 | -------------------------------------------------------------------------------- /desk/sur/spaces/path.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | :: 3 | +$ name cord 4 | +$ path [ship=ship space=name] 5 | -- -------------------------------------------------------------------------------- /desk/sur/spaces/store.hoon: -------------------------------------------------------------------------------- 1 | :: sur/spaces/store.hoon 2 | :: Defines the types for the spaces concept. 3 | :: 4 | :: A space is a higher level concept above a %landscape group. 5 | /- membership, spaces-path, visas 6 | |% 7 | :: 8 | +$ space-path path:spaces-path 9 | +$ space-name name:spaces-path 10 | +$ space-description cord 11 | +$ group-space [creator=ship name=@tas title=@t picture=@t color=@ux] 12 | +$ token 13 | $: chain=?(%ethereum %uqbar) 14 | contract=@t 15 | symbol=@t 16 | name=@t 17 | icon=@t 18 | == 19 | :: 20 | +$ theme-mode ?(%dark %light) 21 | +$ theme 22 | $: mode=theme-mode 23 | background-color=@t 24 | accent-color=@t 25 | input-color=@t 26 | dock-color=@t 27 | icon-color=@t 28 | text-color=@t 29 | window-color=@t 30 | wallpaper=@t 31 | == 32 | :: 33 | +$ spaces (map space-path space) 34 | +$ space 35 | $: path=space-path 36 | name=space-name 37 | description=space-description 38 | type=space-type 39 | access=space-access 40 | picture=@t 41 | color=@t :: '#000000' 42 | =archetype 43 | theme=theme 44 | updated-at=@da 45 | == 46 | +$ space-type ?(%group %space %our) 47 | +$ archetype ?(%home %community %creator-dao %service-dao %investment-dao) 48 | +$ space-access ?(%public %antechamber %private) 49 | :: 50 | :: 51 | :: Poke actions 52 | :: 53 | +$ action 54 | $% [%add slug=@t payload=add-payload members=members:membership] 55 | [%update path=space-path payload=edit-payload] 56 | [%remove path=space-path] 57 | [%join path=space-path] 58 | [%leave path=space-path] 59 | [%current path=space-path] :: set the currently opened space 60 | :: [%kicked path=space-path ship=ship] 61 | == 62 | :: 63 | +$ add-payload 64 | $: name=space-name 65 | description=space-description 66 | type=space-type 67 | access=space-access 68 | picture=@t 69 | color=@t :: '#000000' 70 | =archetype 71 | == 72 | :: 73 | +$ edit-payload 74 | $: name=@t 75 | description=@t 76 | access=space-access 77 | picture=@t 78 | color=@t 79 | =theme 80 | == 81 | :: 82 | :: Reaction via watch paths 83 | :: 84 | +$ reaction 85 | $% [%initial =spaces =membership:membership =invitations:visas current=space-path] 86 | [%add =space members=members:membership] 87 | [%replace =space] 88 | [%remove path=space-path] 89 | [%remote-space path=space-path =space =members:membership] 90 | [%current path=space-path] 91 | == 92 | :: 93 | :: Scry views 94 | :: 95 | +$ view :: rename to effects 96 | $% [%spaces =spaces] 97 | [%space =space] 98 | == 99 | :: 100 | -- -------------------------------------------------------------------------------- /desk/sur/spider.hoon: -------------------------------------------------------------------------------- 1 | /+ libstrand=strand 2 | =, strand=strand:libstrand 3 | |% 4 | +$ thread $-(vase _*form:(strand ,vase)) 5 | +$ input [=tid =cage] 6 | +$ tid tid:strand 7 | +$ bowl bowl:strand 8 | +$ http-error 9 | $? %bad-request :: 400 10 | %forbidden :: 403 11 | %nonexistent :: 404 12 | %offline :: 504 13 | == 14 | +$ start-args 15 | $: parent=(unit tid) 16 | use=(unit tid) 17 | =beak 18 | file=term 19 | =vase 20 | == 21 | -- 22 | -------------------------------------------------------------------------------- /desk/sur/station.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | +$ spat (pair ship cord) 3 | :: 4 | +$ action 5 | $% [%all ~] 6 | [%host who=@p] 7 | [%space wat=what sap=spat] 8 | == 9 | :: 10 | +$ what 11 | $? %spaces 12 | %detail 13 | %host 14 | %owners 15 | %pending 16 | %initiates 17 | %members 18 | %administrators 19 | == 20 | -- -------------------------------------------------------------------------------- /desk/sur/tome.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | +$ space @tas :: space name. if no space this is 'our' 3 | +$ app @tas :: app name (reduce namespace collisions). if no app this is 'all' 4 | +$ bucket @tas :: bucket name (with its own permissions). if no bucket this is 'def' 5 | +$ key @t :: key name 6 | +$ value @t :: value in kv store 7 | +$ content @t :: content for feed post / reply 8 | +$ id @t :: uuid for feed post 9 | +$ json-value [%s value] :: value (JSON encoded as a string). Store with %s so we aren't constantly adding it to requests. 10 | +$ ships (set ship) 11 | +$ invited (map ship invite-level) 12 | :: 13 | +$ perm-level 14 | $% %our 15 | %space 16 | %open 17 | %unset 18 | %yes 19 | %no 20 | == 21 | :: 22 | +$ invite-level 23 | $% %read 24 | %write 25 | %admin 26 | %block 27 | == 28 | :: 29 | +$ meta 30 | $: created-by=ship 31 | updated-by=ship 32 | created-at=time 33 | updated-at=time 34 | == 35 | :: 36 | +$ perm [read=perm-level write=perm-level admin=perm-level] 37 | :: 38 | +$ kv-data (map key json-value) 39 | +$ kv-meta (map key meta) 40 | +$ store (map bucket [=perm invites=invited meta=kv-meta data=kv-data]) 41 | :: 42 | +$ feed-ids (map id time) 43 | :: 44 | :: +$ replies ((mop time reply-value) gth) 45 | +$ links (map @t json-value) 46 | :: 47 | :: +$ reply-value 48 | :: $: created-by=ship 49 | :: updated-by=ship 50 | :: created-at=time 51 | :: updated-at=time 52 | :: content=json-value 53 | :: =links 54 | :: == 55 | :: 56 | +$ feed-value 57 | $: =id 58 | created-by=ship 59 | updated-by=ship 60 | created-at=time 61 | updated-at=time 62 | :: 63 | content=json-value 64 | :: =replies 65 | =links 66 | == 67 | :: 68 | +$ log ? 69 | +$ feed-data ((mop time feed-value) gth) 70 | :: if "log", %write permissions can only add, not edit or delete. 71 | :: this makes it act like a log. (admins can still edit or delete anything). 72 | +$ feed (map (pair =bucket =log) [=perm ids=feed-ids invites=invited data=feed-data]) 73 | :: 74 | +$ tome-data [=store =feed] 75 | :: 76 | :: Actions and updates 77 | :: 78 | +$ tome-action 79 | $% [%init-tome ship=@t =space =app] 80 | [%init-kv ship=@t =space =app =bucket =perm] 81 | :: [%unwatch-kv ship=@t =space =app =bucket] someday... 82 | [%init-feed ship=@t =space =app =bucket =log =perm] 83 | == 84 | :: 85 | +$ kv-action 86 | :: can be sent by any ship 87 | $% [%set-value ship=@t =space =app =bucket =key =value] 88 | [%remove-value ship=@t =space =app =bucket =key] 89 | [%clear-kv ship=@t =space =app =bucket] 90 | [%verify-kv ship=@t =space =app =bucket] 91 | :: must not be Tome owner (these are proxies for watching a foreign Tome) 92 | [%watch-kv ship=@t =space =app =bucket] 93 | [%team-kv ship=@t =space =app =bucket] 94 | :: must be Tome owner (manage permissions and invites) 95 | [%perm-kv ship=@t =space =app =bucket =perm] 96 | [%invite-kv ship=@t =space =app =bucket guy=@t level=invite-level] 97 | == 98 | :: 99 | +$ kv-update 100 | $% [%set =key =value] 101 | [%remove =key] 102 | [%clear ~] 103 | :: 104 | [%get value=?(~ json-value)] 105 | [%all data=kv-data] 106 | [%perm write=?(%yes %no) admin=?(%yes %no)] 107 | == 108 | :: 109 | +$ feed-action 110 | :: can be sent by any ship 111 | $% [%new-post ship=@t =space =app =bucket =log =id =content] 112 | [%edit-post ship=@t =space =app =bucket =log =id =content] 113 | [%delete-post ship=@t =space =app =bucket =log =id] 114 | [%clear-feed ship=@t =space =app =bucket =log] 115 | [%verify-feed ship=@t =space =app =bucket =log] 116 | :: must not be Tome owner (these are proxies for watching a foreign Tome) 117 | [%watch-feed ship=@t =space =app =bucket =log] 118 | [%team-feed ship=@t =space =app =bucket =log] 119 | :: must be Tome owner (manage permissions and invites) 120 | [%perm-feed ship=@t =space =app =bucket =log =perm] 121 | [%invite-feed ship=@t =space =app =bucket =log guy=@t level=invite-level] 122 | :: actions for links (anything a foreign ship wants to associate with a post) 123 | [%set-post-link ship=@t =space =app =bucket =log =id =value] 124 | [%remove-post-link ship=@t =space =app =bucket =log =id] 125 | == 126 | :: 127 | +$ feed-update 128 | $% [%new =id =time =ship =content] :: ship is author, time is post time 129 | [%edit =id =time =ship =content] :: ship is who updated, time is update time 130 | [%delete =id =time] 131 | [%clear ~] 132 | :: 133 | [%set-link =id =time ship=@t =value] :: these are @t to make returning them easier 134 | [%remove-link =id =time ship=@t] 135 | :: 136 | [%get post=?(~ feed-value)] 137 | [%all data=feed-data] 138 | [%perm write=?(%yes %no) admin=?(%yes %no)] 139 | == 140 | :: 141 | -- 142 | 143 | -------------------------------------------------------------------------------- /desk/sur/verb.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | +$ event 3 | $% [%on-init ~] 4 | [%on-load ~] 5 | [%on-poke =mark] 6 | [%on-watch =path] 7 | [%on-leave =path] 8 | [%on-agent =wire sign=term] 9 | [%on-arvo =wire vane=term sign=term] 10 | [%on-fail =term] 11 | == 12 | -- -------------------------------------------------------------------------------- /desk/sur/visas.hoon: -------------------------------------------------------------------------------- 1 | /- membership, spc=spaces-path 2 | |% 3 | :: 4 | :: +$ passport 5 | :: $: =roles:membership 6 | :: alias=cord 7 | :: status=status:membership 8 | :: == 9 | :: :: 10 | :: :: $passports: passports (access) to spaces within Realm 11 | :: +$ passports (map ship passport) 12 | :: :: 13 | :: :: $districts: subdivisions of the entire realm universe 14 | :: +$ districts (map path=path:spaces passports) 15 | :: 16 | 17 | +$ invitations (map path:spc invite) 18 | +$ invite 19 | $: inviter=ship 20 | path=path:spc 21 | role=role:membership 22 | message=cord 23 | name=name:spc 24 | type=?(%group %space %our) 25 | picture=@t 26 | color=@t 27 | invited-at=@da 28 | == 29 | :: 30 | 31 | +$ action 32 | $% [%send-invite path=path:spc =ship =role:membership message=@t] 33 | [%accept-invite path=path:spc] 34 | [%decline-invite path=path:spc] 35 | [%invited path=path:spc =invite] 36 | [%stamped path=path:spc] 37 | [%kick-member path=path:spc =ship] 38 | [%revoke-invite path=path:spc] 39 | == 40 | 41 | +$ reaction 42 | $% [%invite-sent path=path:spc =ship =invite =member:membership] 43 | [%invite-received path=path:spc =invite] 44 | [%invite-removed path=path:spc] 45 | [%invite-accepted path=path:spc =ship =member:membership] 46 | [%kicked path=path:spc =ship] 47 | == 48 | :: 49 | +$ view 50 | $% [%invitations invites=invitations] 51 | == 52 | :: 53 | -- -------------------------------------------------------------------------------- /desk/sys.kelvin: -------------------------------------------------------------------------------- 1 | [%zuse 413] -------------------------------------------------------------------------------- /desk/ted/feed-poke-tunnel.hoon: -------------------------------------------------------------------------------- 1 | /- spider, *tome 2 | /+ *strandio, tomelib 3 | =, strand=strand:spider 4 | |% 5 | ++ decode-input 6 | =, dejs:format 7 | |= jon=json 8 | %. jon 9 | %- ot 10 | :~ ship+so :: needs to have no sig 11 | agent+so 12 | json+so 13 | == 14 | -- 15 | ^- thread:spider 16 | |= arg=vase 17 | =/ m (strand ,vase) 18 | ^- form:m 19 | =/ jon=json 20 | (need !<((unit json) arg)) 21 | =/ input (decode-input jon) 22 | =/ ship `@p`(slav %p -.input) 23 | =/ agent `@tas`-.+.input 24 | =/ act (feed-action:dejs:tomelib (need (de-json:html +.+.input))) 25 | :: 26 | :: This returns nothing on failure, 'success' on success. 27 | ;< ~ bind:m (poke [ship agent] [%feed-action !>(act)]) 28 | (pure:m !>(s+'success')) -------------------------------------------------------------------------------- /desk/ted/kv-poke-tunnel.hoon: -------------------------------------------------------------------------------- 1 | /- spider, *tome 2 | /+ *strandio, tomelib 3 | =, strand=strand:spider 4 | |% 5 | ++ decode-input 6 | =, dejs:format 7 | |= jon=json 8 | %. jon 9 | %- ot 10 | :~ ship+so :: needs to have no sig 11 | agent+so 12 | json+so 13 | == 14 | -- 15 | ^- thread:spider 16 | |= arg=vase 17 | =/ m (strand ,vase) 18 | ^- form:m 19 | =/ jon=json 20 | (need !<((unit json) arg)) 21 | =/ input (decode-input jon) 22 | =/ ship `@p`(slav %p -.input) 23 | =/ agent `@tas`-.+.input 24 | =/ act (kv-action:dejs:tomelib (need (de-json:html +.+.input))) 25 | :: 26 | :: This returns nothing on failure, 'success' on success. 27 | ;< ~ bind:m (poke [ship agent] [%kv-action !>(act)]) 28 | (pure:m !>(s+'success')) -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Build generated files 10 | lib 11 | -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | ## Publishing 2 | 3 | - Delete `package-lock.json`, `node_modules`, `lib` 4 | - Bump version in `package.json` 5 | - `npm i` 6 | - `npm publish --access=public .` 7 | -------------------------------------------------------------------------------- /pkg/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | // Sync object 3 | const config: Config.InitialOptions = { 4 | verbose: true, 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest', 7 | }, 8 | } 9 | export default config 10 | -------------------------------------------------------------------------------- /pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@holium/tome-db", 3 | "version": "1.2.2", 4 | "description": "Urbit's composable database primitive", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "scripts": { 11 | "prepublish": "tsc", 12 | "test": "jest --forceExit" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/holium/tome-db.git" 17 | }, 18 | "keywords": [ 19 | "typescript", 20 | "urbit", 21 | "database", 22 | "tome-db" 23 | ], 24 | "author": "ajlamarc@gmail.com", 25 | "license": "MIT", 26 | "homepage": "https://github.com/holium/tome-db#readme", 27 | "devDependencies": { 28 | "@types/jest": "^29.4.0", 29 | "@types/uuid": "^9.0.0", 30 | "jest": "^29.4.3", 31 | "ts-jest": "^29.0.5", 32 | "ts-node": "^10.9.1", 33 | "ts-node-dev": "^2.0.0", 34 | "typescript": "^4.9.3" 35 | }, 36 | "dependencies": { 37 | "@urbit/http-api": "^2.3.0", 38 | "uuid": "^9.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/src/classes/Tome.ts: -------------------------------------------------------------------------------- 1 | import Urbit from '@urbit/http-api' 2 | import { 3 | InviteLevel, 4 | StoreType, 5 | Value, 6 | Content, 7 | SubscribeUpdate, 8 | FeedlogUpdate, 9 | FeedlogEntry, 10 | Perm, 11 | StoreOptions, 12 | TomeOptions, 13 | InitStoreOptions, 14 | } from '../types' 15 | import { tomeMark, kvMark, feedMark, kvThread, feedThread } from './constants' 16 | import { v4 as uuid, validate } from 'uuid' 17 | 18 | export class Tome { 19 | protected api: Urbit 20 | protected mars: boolean 21 | 22 | protected ourShip: string 23 | protected tomeShip: string 24 | protected space: string 25 | protected spaceForPath: string // space name as woad (encoded) 26 | protected app: string 27 | protected agent: string 28 | protected perm: Perm 29 | protected locked: boolean // if true, Tome is locked to the initial ship and space. 30 | protected inRealm: boolean 31 | 32 | protected static async initTomePoke( 33 | api: Urbit, 34 | ship: string, 35 | space: string, 36 | app: string, 37 | agent: string 38 | ) { 39 | await api.poke({ 40 | app: agent, 41 | mark: tomeMark, 42 | json: { 43 | 'init-tome': { 44 | ship, 45 | space, 46 | app, 47 | }, 48 | }, 49 | onError: (error) => { 50 | throw new Error( 51 | `Tome: Initializing Tome on ship ${ship} failed. Make sure the ship and Tome agent are both running.\nError: ${error}` 52 | ) 53 | }, 54 | }) 55 | } 56 | 57 | // maybe use a different (sub) type here? 58 | protected constructor(options?: InitStoreOptions) { 59 | if (typeof options.api !== 'undefined') { 60 | this.mars = true 61 | const { 62 | api, 63 | tomeShip, 64 | ourShip, 65 | space, 66 | spaceForPath, 67 | app, 68 | agent, 69 | perm, 70 | locked, 71 | inRealm, 72 | } = options 73 | this.api = api 74 | this.tomeShip = tomeShip 75 | this.ourShip = ourShip 76 | this.space = space 77 | this.spaceForPath = spaceForPath 78 | this.app = app 79 | this.agent = agent 80 | this.perm = perm 81 | this.locked = locked 82 | this.inRealm = inRealm 83 | } else { 84 | const { app, tomeShip, ourShip } = options ?? {} 85 | this.mars = false 86 | this.app = app 87 | this.tomeShip = tomeShip 88 | this.ourShip = ourShip 89 | } 90 | } 91 | 92 | /** 93 | * @param api The optional Urbit connection to be used for requests. 94 | * @param app An optional app name to store under. Defaults to `'all'`. 95 | * @param options Optional ship, space, agent name, and permissions for initializing a Tome. 96 | * @returns A new Tome instance. 97 | */ 98 | static async init( 99 | api?: Urbit, 100 | app?: string, 101 | options: TomeOptions = {} 102 | ): Promise { 103 | const mars = typeof api !== 'undefined' 104 | const appName = app ?? 'all' 105 | if (mars) { 106 | let locked = false 107 | const inRealm = options.realm ?? false 108 | let tomeShip = options.ship ?? api.ship 109 | let space = options.space ?? 'our' 110 | const agent = options.agent ?? 'tome' 111 | let spaceForPath = space 112 | 113 | if (inRealm) { 114 | if (options.ship && options.space) { 115 | locked = true 116 | } else if (!options.ship && !options.space) { 117 | try { 118 | const current = await api.scry({ 119 | app: 'spaces', 120 | path: '/current', 121 | }) 122 | space = current.current.space 123 | 124 | const path = current.current.path.split('/') 125 | tomeShip = path[1] 126 | spaceForPath = path[2] 127 | } catch (e) { 128 | throw new Error( 129 | 'Tome: no current space found. Make sure Realm is installed / configured, ' + 130 | 'or pass `realm: false` to `Tome.init`.' 131 | ) 132 | } 133 | } else { 134 | throw new Error( 135 | 'Tome: `ship` and `space` must neither or both be specified when using Realm.' 136 | ) 137 | } 138 | } else { 139 | if (spaceForPath !== 'our') { 140 | throw new Error( 141 | "Tome: only the 'our' space is currently supported when not using Realm. " + 142 | 'If this is needed, please open an issue.' 143 | ) 144 | } 145 | } 146 | 147 | if (!tomeShip.startsWith('~')) { 148 | tomeShip = `~${tomeShip}` 149 | } 150 | // save api.ship so we know who we are. 151 | const ourShip = `~${api.ship}` 152 | const perm = options.permissions 153 | ? options.permissions 154 | : ({ read: 'our', write: 'our', admin: 'our' } as const) 155 | await Tome.initTomePoke(api, tomeShip, space, app, agent) 156 | return new Tome({ 157 | api, 158 | tomeShip, 159 | ourShip, 160 | space, 161 | spaceForPath, 162 | app: appName, 163 | agent, 164 | perm, 165 | locked, 166 | inRealm, 167 | }) 168 | } 169 | return new Tome({ app: appName, tomeShip: 'zod', ourShip: 'zod' }) 170 | } 171 | 172 | private async _initStore( 173 | options: StoreOptions, 174 | type: StoreType, 175 | isLog: boolean 176 | ) { 177 | let permissions = options.permissions ? options.permissions : this.perm 178 | if (this.app === 'all' && this.isOurStore()) { 179 | console.warn( 180 | `Tome-${type}: Permissions on 'all' are ignored. Setting permissions levels to 'our' instead...` 181 | ) 182 | permissions = { 183 | read: 'our', 184 | write: 'our', 185 | admin: 'our', 186 | } as const 187 | } 188 | return await DataStore.initDataStore({ 189 | type, 190 | api: this.api, 191 | tomeShip: this.tomeShip, 192 | ourShip: this.ourShip, 193 | space: this.space, 194 | spaceForPath: this.spaceForPath, 195 | app: this.app, 196 | agent: this.agent, 197 | perm: permissions, 198 | locked: this.locked, 199 | bucket: options.bucket ?? 'def', 200 | preload: options.preload ?? true, 201 | onReadyChange: options.onReadyChange, 202 | onLoadChange: options.onLoadChange, 203 | onWriteChange: options.onWriteChange, 204 | onAdminChange: options.onAdminChange, 205 | onDataChange: options.onDataChange, 206 | isLog, 207 | inRealm: this.inRealm, 208 | }) 209 | } 210 | 211 | /** 212 | * Initialize or connect to a key-value store. 213 | * 214 | * @param options Optional bucket, permissions, preload flag, and callbacks for the store. `permisssions` 215 | * defaults to the Tome's permissions, `bucket` to `'def'`, and `preload` to `true`. 216 | * @returns A `KeyValueStore`. 217 | */ 218 | public async keyvalue(options: StoreOptions = {}): Promise { 219 | if (this.mars) { 220 | return (await this._initStore( 221 | options, 222 | 'kv', 223 | false 224 | )) as KeyValueStore 225 | } 226 | return new KeyValueStore({ 227 | app: this.app, 228 | bucket: options.bucket ?? 'def', 229 | preload: options.preload ?? true, 230 | onDataChange: options.onDataChange, 231 | onLoadChange: options.onLoadChange, 232 | type: 'kv', 233 | }) 234 | } 235 | 236 | /** 237 | * Initialize or connect to a feed store. 238 | * 239 | * @param options Optional bucket, permissions, preload flag, and callbacks for the feed. `permisssions` 240 | * defaults to the Tome's permissions, `bucket` to `'def'`, and `preload` to `true`. 241 | * @returns A `FeedStore`. 242 | */ 243 | public async feed(options: StoreOptions = {}): Promise { 244 | if (this.mars) { 245 | return (await this._initStore(options, 'feed', false)) as FeedStore 246 | } 247 | return new FeedStore({ 248 | app: this.app, 249 | bucket: options.bucket ?? 'def', 250 | preload: options.preload ?? true, 251 | onDataChange: options.onDataChange, 252 | onLoadChange: options.onLoadChange, 253 | isLog: false, 254 | type: 'feed', 255 | ourShip: 'zod', 256 | }) 257 | } 258 | 259 | /** 260 | * Initialize or connect to a log store. 261 | * 262 | * @param options Optional bucket, permissions, preload flag, and callbacks for the log. `permisssions` 263 | * defaults to the Tome's permissions, `bucket` to `'def'`, and `preload` to `true`. 264 | * @returns A `LogStore`. 265 | */ 266 | public async log(options: StoreOptions = {}): Promise { 267 | if (this.mars) { 268 | return (await this._initStore(options, 'feed', true)) as LogStore 269 | } 270 | return new LogStore({ 271 | app: this.app, 272 | bucket: options.bucket ?? 'def', 273 | preload: options.preload ?? true, 274 | onDataChange: options.onDataChange, 275 | onLoadChange: options.onLoadChange, 276 | isLog: true, 277 | type: 'feed', 278 | ourShip: 'zod', 279 | }) 280 | } 281 | 282 | public isOurStore(): boolean { 283 | return this.tomeShip === this.ourShip 284 | } 285 | } 286 | 287 | export abstract class DataStore extends Tome { 288 | protected storeSubscriptionID: number 289 | protected spaceSubscriptionID: number 290 | // if preload is set, loaded will be set to true once the initial subscription state has been received. 291 | // then we know we can use the cache. 292 | protected preload: boolean 293 | protected loaded: boolean 294 | protected ready: boolean // if false, we are switching spaces. 295 | protected onReadyChange: (ready: boolean) => void 296 | protected onLoadChange: (loaded: boolean) => void 297 | protected onWriteChange: (write: boolean) => void 298 | protected onAdminChange: (admin: boolean) => void 299 | protected onDataChange: (data: any) => void 300 | 301 | protected cache: Map // cache key-value pairs 302 | protected feedlog: FeedlogEntry[] // array of objects (feed entries) 303 | protected order: string[] // ids of feed entries in order 304 | 305 | protected bucket: string 306 | protected write: boolean 307 | protected admin: boolean 308 | 309 | protected type: StoreType 310 | protected isLog: boolean 311 | 312 | // assumes MARZ 313 | public static async initDataStore( 314 | options: InitStoreOptions 315 | ): Promise { 316 | const { tomeShip, ourShip, type, isLog } = options 317 | if (tomeShip === ourShip) { 318 | await DataStore.initBucket(options) 319 | const newOptions = { ...options, write: true, admin: true } 320 | switch (type) { 321 | case 'kv': 322 | return new KeyValueStore(newOptions) 323 | case 'feed': 324 | if (isLog) { 325 | return new LogStore(options) 326 | } else { 327 | return new FeedStore(options) 328 | } 329 | } 330 | } 331 | await DataStore.checkExistsAndCanRead(options) 332 | const foreignPerm = { 333 | read: 'yes', 334 | write: 'unset', 335 | admin: 'unset', 336 | } as const 337 | await DataStore.initBucket({ ...options, perm: foreignPerm }) 338 | await DataStore.startWatchingForeignBucket(options) 339 | await DataStore._getCurrentForeignPerms(options) 340 | switch (type) { 341 | case 'kv': 342 | return new KeyValueStore(options) 343 | case 'feed': 344 | if (isLog) { 345 | return new LogStore(options) 346 | } else { 347 | return new FeedStore(options) 348 | } 349 | } 350 | } 351 | 352 | constructor(options?: InitStoreOptions) { 353 | super(options) 354 | const { 355 | bucket, 356 | write, 357 | admin, 358 | preload, 359 | onReadyChange, 360 | onLoadChange, 361 | onWriteChange, 362 | onAdminChange, 363 | onDataChange, 364 | type, 365 | isLog, 366 | } = options ?? {} 367 | this.bucket = bucket 368 | this.preload = preload 369 | this.type = type 370 | this.write = write 371 | this.admin = admin 372 | this.onDataChange = onDataChange 373 | this.onLoadChange = onLoadChange 374 | this.onReadyChange = onReadyChange 375 | this.onWriteChange = onWriteChange 376 | this.onAdminChange = onAdminChange 377 | this.cache = new Map() 378 | this.feedlog = [] 379 | this.order = [] 380 | if (preload) { 381 | this.setLoaded(false) 382 | } 383 | if (type === 'feed') { 384 | this.isLog = isLog 385 | } else { 386 | this.isLog = false 387 | } 388 | if (this.mars) { 389 | if (preload) { 390 | this.subscribeAll() 391 | } 392 | if (this.inRealm) { 393 | this.watchCurrentSpace() 394 | } 395 | this.watchPerms() 396 | this.setReady(true) 397 | } else { 398 | if (preload) { 399 | this.getAllLocalValues() 400 | } 401 | } 402 | } 403 | 404 | private async watchPerms(): Promise { 405 | await this.api.subscribe({ 406 | app: this.agent, 407 | path: this.permsPath(), 408 | err: () => { 409 | console.error( 410 | `Tome-${this.type}: unable to watch perms for this bucket.` 411 | ) 412 | }, 413 | event: async (perms: Perm) => { 414 | if (perms.write !== 'unset') { 415 | const write = perms.write === 'yes' 416 | this.setWrite(write) 417 | } 418 | if (perms.admin !== 'unset') { 419 | const admin = perms.admin === 'yes' 420 | this.setAdmin(admin) 421 | } 422 | }, 423 | quit: async () => await this.watchPerms(), 424 | }) 425 | } 426 | 427 | private async watchCurrentSpace(): Promise { 428 | this.spaceSubscriptionID = await this.api.subscribe({ 429 | app: 'spaces', 430 | path: '/current', 431 | err: () => { 432 | throw new Error( 433 | `Tome-${this.type}: unable to watch current space in spaces agent. Is Realm installed and configured?` 434 | ) 435 | }, 436 | event: async (current: { 437 | current: { path: string; space: string } 438 | }) => { 439 | const space = current.current.space 440 | const path = current.current.path.split('/') 441 | const tomeShip = path[1] 442 | const spaceForPath = path[2] 443 | if (tomeShip !== this.tomeShip || space !== this.space) { 444 | if (this.locked) { 445 | throw new Error( 446 | `Tome-${this.type}: spaces cannot be switched while using a locked Tome.` 447 | ) 448 | } 449 | await this._wipeAndChangeSpace( 450 | tomeShip, 451 | space, 452 | spaceForPath 453 | ) 454 | } 455 | }, 456 | quit: async () => await this.watchCurrentSpace(), 457 | }) 458 | } 459 | 460 | // this seems like pretty dirty update method, is there a better way? 461 | private async _wipeAndChangeSpace( 462 | tomeShip: string, 463 | space: string, 464 | spaceForPath: string 465 | ): Promise { 466 | this.setReady(false) 467 | if (this.storeSubscriptionID) { 468 | await this.api.unsubscribe(this.storeSubscriptionID) 469 | } 470 | // changing the top level tome, so we reinitialize 471 | await Tome.initTomePoke(this.api, tomeShip, space, this.app, this.agent) 472 | const perm = 473 | tomeShip === this.ourShip 474 | ? this.perm 475 | : ({ read: 'yes', write: 'unset', admin: 'unset' } as const) 476 | 477 | const options = { 478 | api: this.api, 479 | tomeShip, 480 | space, 481 | app: this.app, 482 | agent: this.agent, 483 | bucket: this.bucket, 484 | type: this.type, 485 | isLog: this.isLog, 486 | perm, 487 | } 488 | // if not ours, we need to make sure we have read access first. 489 | if (tomeShip !== this.ourShip) { 490 | await DataStore.checkExistsAndCanRead(options) 491 | } 492 | // that succeeded, whether ours or not initialize the bucket. 493 | await DataStore.initBucket(options) 494 | // if not us, we want Hoon side to start a subscription. 495 | if (tomeShip !== this.ourShip) { 496 | await DataStore.startWatchingForeignBucket(options) 497 | } 498 | 499 | this.tomeShip = tomeShip 500 | this.space = space 501 | this.spaceForPath = spaceForPath 502 | this.wipeLocalValues() 503 | if (this.preload) { 504 | this.setLoaded(false) 505 | await this.subscribeAll() 506 | } 507 | 508 | if (this.isOurStore()) { 509 | this.write = true 510 | this.admin = true 511 | } else { 512 | await this.watchPerms() 513 | } 514 | this.setReady(true) 515 | } 516 | 517 | protected static async checkExistsAndCanRead( 518 | options: InitStoreOptions 519 | ): Promise { 520 | const { api, tomeShip, space, app, agent, bucket, type, isLog } = 521 | options 522 | const action = `verify-${type}` 523 | const body = { 524 | [action]: { 525 | ship: tomeShip, 526 | space, 527 | app, 528 | bucket, 529 | }, 530 | } 531 | if (type === 'feed') { 532 | // @ts-expect-error 533 | body[action].log = isLog 534 | } 535 | // Tunnel poke to Tome ship 536 | try { 537 | const result = await api.thread({ 538 | inputMark: 'json', 539 | outputMark: 'json', 540 | threadName: `${type}-poke-tunnel`, 541 | body: { 542 | ship: tomeShip, 543 | agent: agent, 544 | json: JSON.stringify(body), 545 | }, 546 | desk: agent, 547 | }) 548 | const success = result === 'success' 549 | if (!success) { 550 | throw new Error( 551 | `Tome-${type}: the requested bucket does not exist, or you do not have permission to access it.` 552 | ) 553 | } 554 | } catch (e) { 555 | throw new Error( 556 | `Tome-${type}: the requested bucket does not exist, or you do not have permission to access it.` 557 | ) 558 | } 559 | } 560 | 561 | protected static async initBucket( 562 | options: InitStoreOptions 563 | ): Promise { 564 | const { api, tomeShip, space, app, agent, bucket, type, isLog, perm } = 565 | options 566 | const action = `init-${type}` 567 | const body = { 568 | [action]: { 569 | ship: tomeShip, 570 | space, 571 | app, 572 | bucket, 573 | perm, 574 | }, 575 | } 576 | if (type === 'feed') { 577 | // @ts-expect-error 578 | body[action].log = isLog 579 | } 580 | await api.poke({ 581 | app: agent, 582 | mark: tomeMark, 583 | json: body, 584 | onError: (error) => { 585 | throw new Error( 586 | `Tome-${type}: Initializing store on ship ${tomeShip} failed. Make sure the ship and Tome agent are both running.\nError: ${error}` 587 | ) 588 | }, 589 | }) 590 | } 591 | 592 | protected static async startWatchingForeignBucket( 593 | options: InitStoreOptions 594 | ): Promise { 595 | const { api, tomeShip, space, app, agent, bucket, type, isLog } = 596 | options 597 | const action = `watch-${type}` 598 | const mark = type === 'kv' ? kvMark : feedMark 599 | const body = { 600 | [action]: { 601 | ship: tomeShip, 602 | space, 603 | app, 604 | bucket, 605 | }, 606 | } 607 | if (type === 'feed') { 608 | // @ts-expect-error 609 | body[action].log = isLog 610 | } 611 | await api.poke({ 612 | app: agent, 613 | mark, 614 | json: body, 615 | onError: (error) => { 616 | throw new Error( 617 | `Tome-${type}: Starting foreign store watch failed. Make sure the ship and Tome agent are both running.\nError: ${error}` 618 | ) 619 | }, 620 | }) 621 | } 622 | 623 | // called by subclasses 624 | protected async getCurrentForeignPerms() { 625 | // do nothing if not actually foreign 626 | if (this.isOurStore()) { 627 | return 628 | } 629 | return await DataStore._getCurrentForeignPerms({ 630 | api: this.api, 631 | tomeShip: this.tomeShip, 632 | space: this.space, 633 | app: this.app, 634 | bucket: this.bucket, 635 | type: this.type, 636 | isLog: this.isLog, 637 | }) 638 | } 639 | 640 | private static async _getCurrentForeignPerms( 641 | options: InitStoreOptions 642 | ): Promise { 643 | const { api, tomeShip, space, app, agent, bucket, type, isLog } = 644 | options 645 | const action = `team-${type}` 646 | const mark = type === 'kv' ? kvMark : feedMark 647 | const body = { 648 | [action]: { 649 | ship: tomeShip, 650 | space, 651 | app, 652 | bucket, 653 | }, 654 | } 655 | if (type === 'feed') { 656 | // @ts-expect-error 657 | body[action].log = isLog 658 | } 659 | await api.poke({ 660 | app: agent, 661 | mark, 662 | json: body, 663 | onError: (error) => { 664 | throw new Error( 665 | `Tome-${type}: Starting permissions watch failed. Make sure the ship and Tome agent are both running.\nError: ${error}` 666 | ) 667 | }, 668 | }) 669 | } 670 | 671 | // subscribe to all values in the store, and keep cache synced. 672 | protected async subscribeAll(): Promise { 673 | this.storeSubscriptionID = await this.api.subscribe({ 674 | app: this.agent, 675 | path: this.dataPath(), 676 | err: () => { 677 | throw new Error( 678 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.` 679 | ) 680 | }, 681 | event: async (update: SubscribeUpdate) => { 682 | if (this.type === 'kv') { 683 | await this._handleKvUpdates(update) 684 | } else { 685 | await this._handleFeedUpdates( 686 | update as FeedlogEntry[] | FeedlogUpdate 687 | ) 688 | } 689 | this.dataUpdateCallback() 690 | this.setLoaded(true) 691 | }, 692 | quit: async () => await this.subscribeAll(), 693 | }) 694 | } 695 | 696 | // TODO duplicate logic in KV ALL method 697 | protected getAllLocalValues(): void { 698 | if (this.type === 'kv') { 699 | const map = new Map() 700 | const len = localStorage.length 701 | const startIndex = this.localDataPrefix().length 702 | for (let i = 0; i < len; i++) { 703 | const key = localStorage.key(i) 704 | if (key.startsWith(this.localDataPrefix())) { 705 | const keyName = key.substring(startIndex) // get key without prefix 706 | map.set(keyName, JSON.parse(localStorage.getItem(key))) 707 | } 708 | } 709 | this.cache = map 710 | this.dataUpdateCallback() 711 | this.setLoaded(true) 712 | } else if (this.type === 'feed') { 713 | const feedlog = localStorage.getItem(this.localDataPrefix()) 714 | if (feedlog !== null) { 715 | this.feedlog = JSON.parse(feedlog) 716 | this.feedlog.map((entry: FeedlogEntry) => { 717 | this.order.push(entry.id) 718 | return entry 719 | }) 720 | this.dataUpdateCallback() 721 | } 722 | this.setLoaded(true) 723 | } 724 | } 725 | 726 | /** 727 | * Set new permission levels for a store after initialization. 728 | * 729 | * @param permissions the new permissions to set. 730 | */ 731 | public async setPermissions(permissions: Perm): Promise { 732 | if (!this.isOurStore()) { 733 | throw new Error( 734 | `Tome-${this.type}: You can only set permissions on your own ship's store.` 735 | ) 736 | } 737 | const action = `perm-${this.type}` 738 | const mark = this.type === 'kv' ? kvMark : feedMark 739 | const body = { 740 | [action]: { 741 | ship: this.tomeShip, 742 | space: this.space, 743 | app: this.app, 744 | bucket: this.bucket, 745 | perm: permissions, 746 | }, 747 | } 748 | if (this.type === 'feed') { 749 | // @ts-expect-error 750 | body[action].log = this.isLog 751 | } 752 | await this.api.poke({ 753 | app: this.agent, 754 | mark, 755 | json: body, 756 | onError: (error) => { 757 | throw new Error( 758 | `Tome-${this.type}: Updating permissions failed.\nError: ${error}` 759 | ) 760 | }, 761 | }) 762 | } 763 | 764 | /** 765 | * Set permission level for a specific ship. This takes precedence over bucket-level permissions. 766 | * 767 | * @param ship The ship to set permissions for. 768 | * @param level The permission level to set. 769 | */ 770 | public async inviteShip(ship: string, level: InviteLevel): Promise { 771 | if (!this.isOurStore()) { 772 | throw new Error( 773 | `Tome-${this.type}: You can only manage permissions on your own ship's store.` 774 | ) 775 | } 776 | if (!ship.startsWith('~')) { 777 | ship = `~${ship}` 778 | } 779 | const action = `invite-${this.type}` 780 | const mark = this.type === 'kv' ? kvMark : feedMark 781 | const body = { 782 | [action]: { 783 | ship: this.tomeShip, 784 | space: this.space, 785 | app: this.app, 786 | bucket: this.bucket, 787 | guy: ship, 788 | level, 789 | }, 790 | } 791 | if (this.type === 'feed') { 792 | // @ts-expect-error 793 | body[action].log = this.isLog 794 | } 795 | await this.api.poke({ 796 | app: this.agent, 797 | mark, 798 | json: body, 799 | onError: (error) => { 800 | throw new Error( 801 | `Tome-${this.type}: Setting ship permissions failed.\nError: ${error}` 802 | ) 803 | }, 804 | }) 805 | } 806 | 807 | /** 808 | * Block a specific ship from accessing this store. 809 | * 810 | * @param ship The ship to block. 811 | */ 812 | public async blockShip(ship: string): Promise { 813 | await this.inviteShip(ship, 'block') 814 | } 815 | 816 | protected dataPath(key?: string): string { 817 | let path = `/${this.type}/${this.tomeShip}/${this.spaceForPath}/${this.app}/${this.bucket}/` 818 | if (this.type === 'feed') { 819 | path += this.isLog ? 'log/' : 'feed/' 820 | } 821 | path += 'data/' 822 | if (key) { 823 | path += `key/${key}` 824 | } else { 825 | path += 'all' 826 | } 827 | return path 828 | } 829 | 830 | protected localDataPrefix(key?: string): string { 831 | let type: string = this.type 832 | if (this.isLog) { 833 | type = 'log' 834 | } 835 | let path = `/tome-db/${type}/${this.app}/${this.bucket}/` 836 | if (key) { 837 | path += key 838 | } 839 | return path 840 | } 841 | 842 | protected permsPath(): string { 843 | let path = `/${this.type}/${this.tomeShip}/${this.spaceForPath}/${this.app}/${this.bucket}/` 844 | if (this.type === 'feed') { 845 | path += this.isLog ? 'log/' : 'feed/' 846 | } 847 | path += 'perm' 848 | return path 849 | } 850 | 851 | protected setReady(ready: boolean): void { 852 | if (ready !== this.ready) { 853 | this.ready = ready 854 | if (this.onReadyChange) { 855 | this.onReadyChange(ready) 856 | } 857 | } 858 | } 859 | 860 | protected setLoaded(loaded: boolean): void { 861 | if (loaded !== this.loaded) { 862 | this.loaded = loaded 863 | if (this.onLoadChange) { 864 | this.onLoadChange(loaded) 865 | } 866 | } 867 | } 868 | 869 | protected setWrite(write: boolean): void { 870 | if (write !== this.write) { 871 | this.write = write 872 | if (this.onWriteChange) { 873 | this.onWriteChange(write) 874 | } 875 | } 876 | } 877 | 878 | protected setAdmin(admin: boolean): void { 879 | if (admin !== this.admin) { 880 | this.admin = admin 881 | if (this.onAdminChange) { 882 | this.onAdminChange(admin) 883 | } 884 | } 885 | } 886 | 887 | protected async waitForReady(): Promise { 888 | return await new Promise((resolve) => { 889 | while (!this.ready) { 890 | setTimeout(() => {}, 50) 891 | } 892 | resolve() 893 | }) 894 | } 895 | 896 | protected async waitForLoaded(): Promise { 897 | return await new Promise((resolve) => { 898 | while (!this.loaded) { 899 | setTimeout(() => {}, 50) 900 | } 901 | resolve() 902 | }) 903 | } 904 | 905 | protected canStore(value: any): boolean { 906 | if ( 907 | value.constructor === Array || 908 | value.constructor === Object || 909 | value.constructor === String || 910 | value.constructor === Number || 911 | value.constructor === Boolean 912 | ) { 913 | return true 914 | } 915 | return false 916 | } 917 | 918 | protected dataUpdateCallback(): void { 919 | switch (this.type) { 920 | case 'kv': 921 | if (this.onDataChange) { 922 | this.onDataChange(this.cache) 923 | } 924 | break 925 | case 'feed': 926 | if (this.onDataChange) { 927 | this.onDataChange(this.feedlog) 928 | } 929 | break 930 | } 931 | } 932 | 933 | // called when switching spaces 934 | protected wipeLocalValues(): void { 935 | this.cache.clear() 936 | this.order.length = 0 937 | this.feedlog.length = 0 938 | this.dataUpdateCallback() 939 | } 940 | 941 | protected parseFeedlogEntry(entry: FeedlogEntry): FeedlogEntry { 942 | entry.createdBy = entry.createdBy.slice(1) 943 | entry.updatedBy = entry.updatedBy.slice(1) 944 | // @ts-ignore 945 | entry.content = JSON.parse(entry.content) 946 | entry.links = Object.fromEntries( 947 | Object.entries(entry.links).map(([k, v]) => [ 948 | k.slice(1), 949 | JSON.parse(v), 950 | ]) 951 | ) 952 | return entry 953 | } 954 | 955 | protected async pokeOrTunnel({ json, onSuccess, onError }) { 956 | await this.waitForReady() 957 | let success = false 958 | if (this.isOurStore()) { 959 | let result: any // what onSuccess or onError returns 960 | await this.api.poke({ 961 | app: this.agent, 962 | mark: this.type === 'kv' ? kvMark : feedMark, 963 | json, 964 | onSuccess: () => { 965 | result = onSuccess() 966 | }, 967 | onError: () => { 968 | result = onError() 969 | }, 970 | }) 971 | return result 972 | } else { 973 | // Tunnel poke to Tome ship 974 | try { 975 | const result = await this.api.thread({ 976 | inputMark: 'json', 977 | outputMark: 'json', 978 | threadName: this.type === 'kv' ? kvThread : feedThread, 979 | body: { 980 | ship: this.tomeShip, 981 | agent: this.agent, 982 | json: JSON.stringify(json), 983 | }, 984 | desk: this.agent, 985 | }) 986 | success = result === 'success' 987 | } catch (e) {} 988 | } 989 | return success ? onSuccess() : onError() 990 | } 991 | 992 | private async _handleKvUpdates(update: object): Promise { 993 | const entries: Array<[string, string]> = Object.entries(update) 994 | if (entries.length === 0) { 995 | // received an empty object, clear the cache. 996 | this.cache.clear() 997 | } else { 998 | for (const [key, value] of entries) { 999 | if (value === null) { 1000 | this.cache.delete(key) 1001 | } else { 1002 | this.cache.set(key, JSON.parse(value)) 1003 | } 1004 | } 1005 | } 1006 | } 1007 | 1008 | private _handleFeedAll(update: FeedlogEntry[]): void { 1009 | update.map((entry: FeedlogEntry) => { 1010 | // save the IDs in time order so they are easier to find later 1011 | this.order.push(entry.id) 1012 | return this.parseFeedlogEntry(entry) 1013 | }) 1014 | this.feedlog = update 1015 | } 1016 | 1017 | private async _handleFeedUpdate(update: FeedlogUpdate): Promise { 1018 | await this.waitForLoaded() 1019 | // %new, %edit, %delete, %clear, %set-link, %remove-link 1020 | let index: number 1021 | switch (update.type) { 1022 | case 'new': { 1023 | this.order.unshift(update.body.id) 1024 | const ship = update.body.ship.slice(1) 1025 | const entry = { 1026 | id: update.body.id, 1027 | createdAt: update.body.time, 1028 | updatedAt: update.body.time, 1029 | createdBy: ship, 1030 | updatedBy: ship, 1031 | // @ts-ignore 1032 | content: JSON.parse(update.body.content), 1033 | links: {}, 1034 | } 1035 | this.feedlog.unshift(entry) 1036 | break 1037 | } 1038 | case 'edit': 1039 | index = this.order.indexOf(update.body.id) 1040 | if (index > -1) { 1041 | this.feedlog[index] = { 1042 | ...this.feedlog[index], 1043 | // @ts-ignore 1044 | content: JSON.parse(update.body.content), 1045 | updatedAt: update.body.time, 1046 | updatedBy: update.body.ship.slice(1), 1047 | } 1048 | } 1049 | break 1050 | case 'delete': 1051 | index = this.order.indexOf(update.body.id) 1052 | if (index > -1) { 1053 | this.feedlog.splice(index, 1) 1054 | this.order.splice(index, 1) 1055 | } 1056 | break 1057 | case 'clear': 1058 | this.wipeLocalValues() 1059 | break 1060 | case 'set-link': 1061 | index = this.order.indexOf(update.body.id) 1062 | if (index > -1) { 1063 | this.feedlog[index] = { 1064 | ...this.feedlog[index], 1065 | links: { 1066 | ...this.feedlog[index].links, 1067 | [update.body.ship.slice(1)]: JSON.parse( 1068 | update.body.value 1069 | ), 1070 | }, 1071 | } 1072 | } 1073 | break 1074 | case 'remove-link': 1075 | index = this.order.indexOf(update.body.id) 1076 | if (index > -1) { 1077 | this.feedlog[index] = { 1078 | ...this.feedlog[index], 1079 | // @ts-ignore 1080 | links: (({ [update.body.ship.slice(1)]: _, ...o }) => 1081 | o)(this.feedlog[index].links), // remove data.body.ship 1082 | } 1083 | } 1084 | break 1085 | default: 1086 | console.error('Tome-feed: unknown update type') 1087 | } 1088 | } 1089 | 1090 | private async _handleFeedUpdates( 1091 | update: FeedlogEntry[] | FeedlogUpdate 1092 | ): Promise { 1093 | if (update.constructor === Array) { 1094 | this._handleFeedAll(update) 1095 | } else { 1096 | await this._handleFeedUpdate(update as FeedlogUpdate) 1097 | } 1098 | } 1099 | } 1100 | 1101 | export abstract class FeedlogStore extends DataStore { 1102 | name: 'feed' | 'log' 1103 | 1104 | constructor(options: InitStoreOptions) { 1105 | super(options) 1106 | options.isLog ? (this.name = 'log') : (this.name = 'feed') 1107 | } 1108 | 1109 | private async _postOrEdit( 1110 | content: Content, 1111 | id?: string 1112 | ): Promise { 1113 | const action = typeof id === 'undefined' ? 'new-post' : 'edit-post' 1114 | if (action === 'new-post') { 1115 | id = uuid() 1116 | } else { 1117 | if (!validate(id)) { 1118 | console.error('Invalid ID.') 1119 | return undefined 1120 | } 1121 | } 1122 | if (!this.canStore(content)) { 1123 | console.error('content is an invalid type.') 1124 | return undefined 1125 | } 1126 | const contentStr = JSON.stringify(content) 1127 | const json = { 1128 | [action]: { 1129 | ship: this.tomeShip, 1130 | space: this.space, 1131 | app: this.app, 1132 | bucket: this.bucket, 1133 | log: this.isLog, 1134 | id, 1135 | content: contentStr, 1136 | }, 1137 | } 1138 | return await this.pokeOrTunnel({ 1139 | json, 1140 | onSuccess: () => { 1141 | // cache somewhere? 1142 | return id 1143 | }, 1144 | onError: () => { 1145 | console.error( 1146 | `Tome-${this.name}: Failed to save content to the ${this.name}. Checking perms...` 1147 | ) 1148 | this.getCurrentForeignPerms() 1149 | return undefined 1150 | }, 1151 | }) 1152 | } 1153 | 1154 | /** 1155 | * Add a new post to the feedlog. Automatically stores the creation time and author. 1156 | * 1157 | * @param content The Content to post to the feedlog. 1158 | * Can be a string, number, boolean, Array, or JSON. 1159 | * @returns The post ID on success, undefined on failure. 1160 | */ 1161 | public async post(content: Content): Promise { 1162 | if (!this.mars) { 1163 | const now = new Date().getTime() 1164 | const id = uuid() 1165 | this.order.unshift(id) 1166 | const entry = { 1167 | id, 1168 | createdAt: now, 1169 | updatedAt: now, 1170 | createdBy: this.ourShip, 1171 | updatedBy: this.ourShip, 1172 | content, 1173 | links: {}, 1174 | } 1175 | this.feedlog.unshift(entry) 1176 | localStorage.setItem( 1177 | this.localDataPrefix(), 1178 | JSON.stringify(this.feedlog) 1179 | ) 1180 | this.dataUpdateCallback() 1181 | return id 1182 | } 1183 | return await this._postOrEdit(content) 1184 | } 1185 | 1186 | /** 1187 | * Edit a post in the feedlog. Automatically stores the updated time and author. 1188 | * 1189 | * @param id The ID of the post to edit. 1190 | * @param newContent The newContent to replace with. 1191 | * Can be a string, number, boolean, Array, or JSON. 1192 | * @returns The post ID on success, undefined on failure. 1193 | */ 1194 | public async edit( 1195 | id: string, 1196 | newContent: Content 1197 | ): Promise { 1198 | if (!this.mars) { 1199 | const index = this.order.indexOf(id) 1200 | if (index === -1) { 1201 | console.error('ID not found.') 1202 | return undefined 1203 | } 1204 | this.feedlog[index] = { 1205 | ...this.feedlog[index], 1206 | updatedAt: new Date().getTime(), 1207 | content: newContent, 1208 | } 1209 | localStorage.setItem( 1210 | this.localDataPrefix(), 1211 | JSON.stringify(this.feedlog) 1212 | ) 1213 | this.dataUpdateCallback() 1214 | return id 1215 | } else { 1216 | return await this._postOrEdit(newContent, id) 1217 | } 1218 | } 1219 | 1220 | /** 1221 | * Delete a post from the feedlog. If the post with ID does not exist, returns true. 1222 | * 1223 | * @param id The ID of the post to delete. 1224 | * @returns true on success, false on failure. 1225 | */ 1226 | public async delete(id: string): Promise { 1227 | if (!validate(id)) { 1228 | console.error('Invalid ID.') 1229 | return false 1230 | } 1231 | if (!this.mars) { 1232 | const index = this.order.indexOf(id) 1233 | if (index === -1) { 1234 | return true 1235 | } 1236 | this.order.splice(index, 1) 1237 | this.feedlog.splice(index, 1) 1238 | localStorage.setItem( 1239 | this.localDataPrefix(), 1240 | JSON.stringify(this.feedlog) 1241 | ) 1242 | this.dataUpdateCallback() 1243 | return true 1244 | } 1245 | const json = { 1246 | 'delete-post': { 1247 | ship: this.tomeShip, 1248 | space: this.space, 1249 | app: this.app, 1250 | bucket: this.bucket, 1251 | log: this.isLog, 1252 | id, 1253 | }, 1254 | } 1255 | return await this.pokeOrTunnel({ 1256 | json, 1257 | onSuccess: () => { 1258 | // cache somewhere? 1259 | return true 1260 | }, 1261 | onError: () => { 1262 | console.error( 1263 | `Tome-${this.name}: Failed to delete post from ${this.name}. Checking perms...` 1264 | ) 1265 | this.getCurrentForeignPerms() 1266 | return false 1267 | }, 1268 | }) 1269 | } 1270 | 1271 | /** 1272 | * Clear all posts from the feedlog. 1273 | * 1274 | * @returns true on success, false on failure. 1275 | */ 1276 | public async clear(): Promise { 1277 | if (!this.mars) { 1278 | this.wipeLocalValues() 1279 | localStorage.removeItem(this.localDataPrefix()) 1280 | this.dataUpdateCallback() 1281 | return true 1282 | } 1283 | const json = { 1284 | 'clear-feed': { 1285 | ship: this.tomeShip, 1286 | space: this.space, 1287 | app: this.app, 1288 | bucket: this.bucket, 1289 | log: this.isLog, 1290 | }, 1291 | } 1292 | return await this.pokeOrTunnel({ 1293 | json, 1294 | onSuccess: () => { 1295 | // cache somewhere? 1296 | return true 1297 | }, 1298 | onError: () => { 1299 | console.error( 1300 | `Tome-${this.name}: Failed to clear ${this.name}. Checking perms...` 1301 | ) 1302 | this.getCurrentForeignPerms() 1303 | return false 1304 | }, 1305 | }) 1306 | } 1307 | 1308 | /** 1309 | * Get the post from the feedlog with the given ID. 1310 | * 1311 | * @param id The ID of the post to retrieve. 1312 | * @param allowCachedValue If true, will return the cached value if it exists. 1313 | * If false, will always fetch from Urbit. Defaults to true. 1314 | * @returns A FeedlogEntry on success, undefined on failure. 1315 | */ 1316 | public async get( 1317 | id: string, 1318 | allowCachedValue: boolean = true 1319 | ): Promise { 1320 | if (!this.mars) { 1321 | throw new Error( 1322 | 'Tome: get() is not supported in local mode. Please create an issue on GitHub if you need this feature.' 1323 | ) 1324 | } 1325 | if (!validate(id)) { 1326 | console.error('Invalid ID.') 1327 | return undefined 1328 | } 1329 | await this.waitForReady() 1330 | if (allowCachedValue) { 1331 | const index = this.order.indexOf(id) 1332 | if (index > -1) { 1333 | return this.feedlog[index] 1334 | } 1335 | } 1336 | if (this.preload) { 1337 | await this.waitForLoaded() 1338 | const index = this.order.indexOf(id) 1339 | if (index === -1) { 1340 | console.error(`id ${id} not found`) 1341 | return undefined 1342 | } 1343 | return this.feedlog[index] 1344 | } else { 1345 | return await this._getValueFromUrbit(id) 1346 | } 1347 | } 1348 | 1349 | /** 1350 | * Retrieve all posts from the feedlog, sorted by newest first. 1351 | * 1352 | * @param useCache If true, return the current cache instead of querying Urbit. 1353 | * Only relevant if preload was set to false. Defaults to false. 1354 | * @returns A FeedlogEntry on success, undefined on failure. 1355 | */ 1356 | public async all(useCache: boolean = false): Promise { 1357 | if (!this.mars) { 1358 | const posts = localStorage.getItem(this.localDataPrefix()) 1359 | if (posts === null) { 1360 | return undefined 1361 | } 1362 | return JSON.parse(posts) 1363 | } 1364 | await this.waitForReady() 1365 | if (this.preload) { 1366 | await this.waitForLoaded() 1367 | return this.feedlog 1368 | } 1369 | if (useCache) { 1370 | return this.feedlog 1371 | } 1372 | return await this._getAllFromUrbit() 1373 | } 1374 | 1375 | private async _getValueFromUrbit(id: string): Promise { 1376 | try { 1377 | let post = await this.api.scry({ 1378 | app: this.agent, 1379 | path: this.dataPath(id), 1380 | }) 1381 | if (post === null) { 1382 | return undefined 1383 | } 1384 | post = this.parseFeedlogEntry(post) 1385 | const index = this.order.indexOf(id) 1386 | if (index === -1) { 1387 | // TODO find the actual right place to insert this (based on times)? 1388 | this.order.unshift(id) 1389 | this.feedlog.unshift(post) 1390 | } else { 1391 | this.feedlog[index] = post 1392 | } 1393 | return post 1394 | } catch (e) { 1395 | throw new Error( 1396 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.` 1397 | ) 1398 | } 1399 | } 1400 | 1401 | private async _getAllFromUrbit(): Promise { 1402 | try { 1403 | const data = await this.api.scry({ 1404 | app: this.agent, 1405 | path: this.dataPath(), 1406 | }) 1407 | // wipe and replace feedlog 1408 | this.order.length = 0 1409 | this.feedlog = data.map((entry: FeedlogEntry) => { 1410 | this.order.push(entry.id) 1411 | return this.parseFeedlogEntry(entry) 1412 | }) 1413 | return this.feedlog 1414 | } catch (e) { 1415 | throw new Error( 1416 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.` 1417 | ) 1418 | } 1419 | } 1420 | 1421 | // other useful methods 1422 | // iterator(page_size = 50) or equivalent for a paginated query 1423 | // since(time: xxx) - returns all posts since time? 1424 | } 1425 | 1426 | export class FeedStore extends FeedlogStore { 1427 | private async _setOrRemoveLink( 1428 | id: string, 1429 | content?: Content 1430 | ): Promise { 1431 | const action = 1432 | typeof content !== 'undefined' 1433 | ? 'set-post-link' 1434 | : 'remove-post-link' 1435 | if (action === 'set-post-link') { 1436 | if (!this.canStore(content)) { 1437 | console.error('value is an invalid type.') 1438 | return false 1439 | } 1440 | } 1441 | const json = { 1442 | [action]: { 1443 | ship: this.tomeShip, 1444 | space: this.space, 1445 | app: this.app, 1446 | bucket: this.bucket, 1447 | log: this.isLog, 1448 | id, 1449 | }, 1450 | } 1451 | if (action === 'set-post-link') { 1452 | // @ts-expect-error 1453 | json[action].value = JSON.stringify(content) 1454 | } 1455 | return await this.pokeOrTunnel({ 1456 | json, 1457 | onSuccess: () => { 1458 | // cache somewhere? 1459 | return true 1460 | }, 1461 | onError: () => { 1462 | console.error( 1463 | `Tome-${this.name}: Failed to modify link in the ${this.name}.` 1464 | ) 1465 | this.getCurrentForeignPerms() 1466 | return false 1467 | }, 1468 | }) 1469 | } 1470 | 1471 | /** 1472 | * Associate a link with the feed post corresponding to ID. 1473 | * 1474 | * @param id The ID of the post to link to. 1475 | * @param content The Content to associate with the post. 1476 | * @returns true on success, false on failure. 1477 | */ 1478 | public async setLink(id: string, content: Content): Promise { 1479 | if (!validate(id)) { 1480 | console.error('Invalid ID.') 1481 | return false 1482 | } 1483 | if (!this.mars) { 1484 | const index = this.order.indexOf(id) 1485 | if (index === -1) { 1486 | console.error('Post does not exist.') 1487 | return false 1488 | } 1489 | this.feedlog[index] = { 1490 | ...this.feedlog[index], 1491 | links: { 1492 | ...this.feedlog[index].links, 1493 | [this.ourShip]: content, 1494 | }, 1495 | } 1496 | localStorage.setItem( 1497 | this.localDataPrefix(), 1498 | JSON.stringify(this.feedlog) 1499 | ) 1500 | this.dataUpdateCallback() 1501 | return true 1502 | } 1503 | return await this._setOrRemoveLink(id, content) 1504 | } 1505 | 1506 | /** 1507 | * Remove the current ship's link to the feed post corresponding to ID. 1508 | * 1509 | * @param id The ID of the post to remove the link from. 1510 | * @returns true on success, false on failure. If the post with ID does not exist, returns true. 1511 | */ 1512 | public async removeLink(id: string): Promise { 1513 | if (!validate(id)) { 1514 | console.error('Invalid ID.') 1515 | return false 1516 | } 1517 | if (!this.mars) { 1518 | const index = this.order.indexOf(id) 1519 | if (index === -1) { 1520 | console.error('Post does not exist.') 1521 | return false 1522 | } 1523 | this.feedlog[index] = { 1524 | ...this.feedlog[index], 1525 | // @ts-expect-error 1526 | links: (({ [this.ourShip]: _, ...o }) => o)( 1527 | this.feedlog[index].links 1528 | ), // remove data.body.ship 1529 | } 1530 | localStorage.setItem( 1531 | this.localDataPrefix(), 1532 | JSON.stringify(this.feedlog) 1533 | ) 1534 | this.dataUpdateCallback() 1535 | return true 1536 | } 1537 | return await this._setOrRemoveLink(id) 1538 | } 1539 | } 1540 | 1541 | export class LogStore extends FeedlogStore {} 1542 | 1543 | export class KeyValueStore extends DataStore { 1544 | /** 1545 | * Set a key-value pair in the store. 1546 | * 1547 | * @param key The key to set. 1548 | * @param value The value to associate with the key. Can be a string, number, boolean, Array, or JSON. 1549 | * @returns true on success, false on failure. 1550 | */ 1551 | public async set(key: string, value: Value): Promise { 1552 | if (!key) { 1553 | console.error('missing key parameter') 1554 | return false 1555 | } 1556 | if (!this.canStore(value)) { 1557 | console.error('value is an invalid type.') 1558 | return false 1559 | } 1560 | const valueStr = JSON.stringify(value) 1561 | 1562 | if (!this.mars) { 1563 | try { 1564 | localStorage.setItem(this.localDataPrefix(key), valueStr) 1565 | this.cache.set(key, value) 1566 | this.dataUpdateCallback() 1567 | return true 1568 | } catch (error) { 1569 | console.error(error) 1570 | return false 1571 | } 1572 | } else { 1573 | const json = { 1574 | 'set-value': { 1575 | ship: this.tomeShip, 1576 | space: this.space, 1577 | app: this.app, 1578 | bucket: this.bucket, 1579 | key, 1580 | value: valueStr, 1581 | }, 1582 | } 1583 | return await this.pokeOrTunnel({ 1584 | json, 1585 | onSuccess: () => { 1586 | this.cache.set(key, value) 1587 | this.dataUpdateCallback() 1588 | return true 1589 | }, 1590 | onError: () => { 1591 | console.error('Failed to set key-value pair in the Store.') 1592 | this.getCurrentForeignPerms() 1593 | return false 1594 | }, 1595 | }) 1596 | } 1597 | } 1598 | 1599 | /** 1600 | * Remove a key-value pair from the store. 1601 | * 1602 | * @param key The key to remove. 1603 | * @returns true on success, false on failure. If the key does not exist, returns true. 1604 | */ 1605 | public async remove(key: string): Promise { 1606 | if (!key) { 1607 | console.error('missing key parameter') 1608 | return false 1609 | } 1610 | if (!this.mars) { 1611 | localStorage.removeItem(this.localDataPrefix(key)) 1612 | this.cache.delete(key) 1613 | this.dataUpdateCallback() 1614 | return true 1615 | } else { 1616 | const json = { 1617 | 'remove-value': { 1618 | ship: this.tomeShip, 1619 | space: this.space, 1620 | app: this.app, 1621 | bucket: this.bucket, 1622 | key, 1623 | }, 1624 | } 1625 | return await this.pokeOrTunnel({ 1626 | json, 1627 | onSuccess: () => { 1628 | this.cache.delete(key) 1629 | this.dataUpdateCallback() 1630 | return true 1631 | }, 1632 | onError: () => { 1633 | console.error( 1634 | 'Failed to remove key-value pair from the Store.' 1635 | ) 1636 | this.getCurrentForeignPerms() 1637 | return false 1638 | }, 1639 | }) 1640 | } 1641 | } 1642 | 1643 | /** 1644 | * Discard all values in the store. 1645 | * 1646 | * @returns true on success, false on failure. 1647 | */ 1648 | public async clear(): Promise { 1649 | if (!this.mars) { 1650 | // TODO - only clear certain keys 1651 | localStorage.clear() 1652 | this.cache.clear() 1653 | this.dataUpdateCallback() 1654 | return true 1655 | } else { 1656 | const json = { 1657 | 'clear-kv': { 1658 | ship: this.tomeShip, 1659 | space: this.space, 1660 | app: this.app, 1661 | bucket: this.bucket, 1662 | }, 1663 | } 1664 | return await this.pokeOrTunnel({ 1665 | json, 1666 | onSuccess: () => { 1667 | this.cache.clear() 1668 | this.dataUpdateCallback() 1669 | return true 1670 | }, 1671 | onError: () => { 1672 | console.error('Failed to clear Store.') 1673 | this.getCurrentForeignPerms() 1674 | return false 1675 | }, 1676 | }) 1677 | } 1678 | } 1679 | 1680 | /** 1681 | * Get the value associated with a key in the store. 1682 | * 1683 | * @param key The key to retrieve. 1684 | * @param allowCachedValue Whether we can check for cached values first. 1685 | * If false, we will always check Urbit for the latest value. Default is true. 1686 | * @returns The value associated with the key, or undefined if the key does not exist. 1687 | */ 1688 | public async get( 1689 | key: string, 1690 | allowCachedValue: boolean = true 1691 | ): Promise { 1692 | if (!key) { 1693 | console.error('missing key parameter') 1694 | return undefined 1695 | } 1696 | 1697 | if (!this.mars) { 1698 | const value = localStorage.getItem(this.localDataPrefix(key)) 1699 | if (value === null) { 1700 | console.error(`key ${key} not found`) 1701 | return undefined 1702 | } 1703 | return JSON.parse(value) 1704 | } else { 1705 | await this.waitForReady() 1706 | // first check cache if allowed 1707 | if (allowCachedValue) { 1708 | const value = this.cache.get(key) 1709 | if (typeof value !== 'undefined') { 1710 | return value 1711 | } 1712 | } 1713 | if (this.preload) { 1714 | await this.waitForLoaded() 1715 | const value = this.cache.get(key) 1716 | if (typeof value === 'undefined') { 1717 | console.error(`key ${key} not found`) 1718 | } 1719 | return value 1720 | } else { 1721 | return await this._getValueFromUrbit(key) 1722 | } 1723 | } 1724 | } 1725 | 1726 | /** 1727 | * Get all key-value pairs in the store. 1728 | * 1729 | * @param useCache return the cache instead of querying Urbit. Only relevant if preload was set to false. 1730 | * @returns A map of all key-value pairs in the store. 1731 | */ 1732 | public async all(useCache: boolean = false): Promise> { 1733 | if (!this.mars) { 1734 | const map = new Map() 1735 | const len = localStorage.length 1736 | const startIndex = this.localDataPrefix().length 1737 | for (let i = 0; i < len; i++) { 1738 | const key = localStorage.key(i) 1739 | if (key.startsWith(this.localDataPrefix())) { 1740 | const keyName = key.substring(startIndex) // get key without prefix 1741 | map.set(keyName, JSON.parse(localStorage.getItem(key))) 1742 | } 1743 | } 1744 | return map 1745 | } else { 1746 | await this.waitForReady() 1747 | if (this.preload) { 1748 | await this.waitForLoaded() 1749 | return this.cache 1750 | } 1751 | if (useCache) { 1752 | return this.cache 1753 | } 1754 | return await this._getAllFromUrbit() 1755 | } 1756 | } 1757 | 1758 | private async _getValueFromUrbit(key: string): Promise { 1759 | try { 1760 | let value = await this.api.scry({ 1761 | app: this.agent, 1762 | path: this.dataPath(key), 1763 | }) 1764 | // TODO value is null when it shouldn't be. 1765 | if (value === null) { 1766 | this.cache.delete(key) 1767 | return undefined 1768 | } 1769 | value = JSON.parse(value) 1770 | this.cache.set(key, value) 1771 | return value 1772 | } catch (e) { 1773 | throw new Error( 1774 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.` 1775 | ) 1776 | } 1777 | } 1778 | 1779 | private async _getAllFromUrbit(): Promise> { 1780 | try { 1781 | const data = await this.api.scry({ 1782 | app: this.agent, 1783 | path: this.dataPath(), 1784 | }) 1785 | this.cache.clear() 1786 | const entries: Array<[string, string]> = Object.entries(data) 1787 | for (const [key, value] of entries) { 1788 | this.cache.set(key, JSON.parse(value)) 1789 | } 1790 | return this.cache 1791 | } catch (e) { 1792 | throw new Error( 1793 | `Tome-${this.type}: the store being used has been removed, or your access has been revoked.` 1794 | ) 1795 | } 1796 | } 1797 | } 1798 | -------------------------------------------------------------------------------- /pkg/src/classes/constants.ts: -------------------------------------------------------------------------------- 1 | export const tomeMark = 'tome-action' 2 | export const kvMark = 'kv-action' 3 | export const feedMark = 'feed-action' 4 | 5 | export const kvThread = 'kv-poke-tunnel' 6 | export const feedThread = 'feed-poke-tunnel' 7 | -------------------------------------------------------------------------------- /pkg/src/classes/index.ts: -------------------------------------------------------------------------------- 1 | export { Tome, DataStore, KeyValueStore, FeedStore, LogStore } from './Tome' 2 | -------------------------------------------------------------------------------- /pkg/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './classes' 3 | import { Tome } from './classes/Tome' 4 | export { Tome as default, Tome } 5 | -------------------------------------------------------------------------------- /pkg/src/types.ts: -------------------------------------------------------------------------------- 1 | import Urbit from '@urbit/http-api' 2 | 3 | type PermLevel = 'our' | 'space' | 'open' | 'unset' | 'yes' | 'no' 4 | export type InviteLevel = 'read' | 'write' | 'admin' | 'block' 5 | export type StoreType = 'kv' | 'feed' 6 | 7 | type T = string | number | boolean | object | T[] 8 | export type Value = T 9 | export type Content = T 10 | 11 | export type SubscribeUpdate = object | FeedlogEntry[] | FeedlogUpdate 12 | 13 | export interface FeedlogUpdate { 14 | type: 'new' | 'edit' | 'delete' | 'clear' | 'set-link' | 'remove-link' 15 | body: FeedlogEntry 16 | } 17 | 18 | export interface FeedlogEntry { 19 | id: string 20 | createdAt: number 21 | updatedAt: number 22 | createdBy: string 23 | updatedBy: string 24 | content: Content 25 | links: object 26 | ship?: string 27 | time?: number 28 | value?: string 29 | } 30 | 31 | export interface Perm { 32 | read: PermLevel 33 | write: PermLevel 34 | admin: PermLevel 35 | } 36 | 37 | export interface TomeOptions { 38 | realm?: boolean 39 | ship?: string 40 | space?: string 41 | agent?: string 42 | permissions?: Perm 43 | } 44 | 45 | export interface StoreOptions { 46 | bucket?: string 47 | permissions?: Perm 48 | preload?: boolean 49 | onReadyChange?: (ready: boolean) => void 50 | onLoadChange?: (loaded: boolean) => void 51 | onWriteChange?: (write: boolean) => void 52 | onAdminChange?: (admin: boolean) => void 53 | onDataChange?: (data: any) => void 54 | } 55 | 56 | export interface InitStoreOptions { 57 | api?: Urbit 58 | tomeShip?: string 59 | space?: string 60 | spaceForPath?: string 61 | app?: string 62 | agent?: string 63 | bucket?: string 64 | type?: StoreType 65 | isLog?: boolean 66 | perm?: Perm 67 | ourShip?: string 68 | locked?: boolean 69 | inRealm?: boolean 70 | preload?: boolean 71 | onReadyChange?: (ready: boolean) => void 72 | onLoadChange?: (loaded: boolean) => void 73 | onWriteChange?: (write: boolean) => void 74 | onAdminChange?: (admin: boolean) => void 75 | onDataChange?: (data: any) => void 76 | write?: boolean 77 | admin?: boolean 78 | } 79 | -------------------------------------------------------------------------------- /pkg/tests/Tome.test.ts: -------------------------------------------------------------------------------- 1 | import Tome from '../src' 2 | import Urbit from '@urbit/http-api' 3 | import { describe, expect, test, beforeAll, afterAll } from '@jest/globals' 4 | 5 | describe('basic local Tome tests', () => { 6 | test('local Tome is ours', async () => { 7 | const db = await Tome.init() 8 | expect(db.isOurStore()).toBe(true) 9 | }) 10 | }) 11 | 12 | describe('basic remote Tome tests', () => { 13 | let api: Urbit 14 | 15 | beforeAll(async () => { 16 | api = await Urbit.authenticate({ 17 | ship: 'zod', 18 | url: 'http://localhost:8080', 19 | code: 'lidlut-tabwed-pillex-ridrup', 20 | }) 21 | }) 22 | 23 | test('remote Tome is ours', async () => { 24 | const db = await Tome.init(api, 'racket', { 25 | ship: 'zod', 26 | }) 27 | expect(db.isOurStore()).toBe(true) 28 | }) 29 | 30 | test('realm tome', async () => { 31 | const db = await Tome.init(api, 'racket', { 32 | realm: true, 33 | }) 34 | expect(db.isOurStore()).toBe(true) 35 | }) 36 | 37 | // this doesn't work yet 38 | afterAll(async () => { 39 | await api.delete() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /pkg/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 4 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "lib": [ 6 | "es5", 7 | "dom", 8 | "es2015", 9 | "es2015.collection", 10 | "ESNext" 11 | ] /* Specify library files to be included in the compilation. */, 12 | "declaration": true /* Generates corresponding '.d.ts' file. */, 13 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 14 | "sourceMap": true /* Generates corresponding '.map' file. */, 15 | "outDir": "lib" /* Redirect output structure to the directory. */, 16 | /* Strict Type-Checking Options */ 17 | "strict": false /* Enable all strict type-checking options. */, 18 | /* Module Resolution Options */ 19 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 20 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 21 | /* Advanced Options */ 22 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 23 | }, 24 | "include": ["src/**/*.ts"], 25 | "exclude": ["node_modules", "lib"] 26 | } 27 | -------------------------------------------------------------------------------- /quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ## Client Install 4 | 5 | {% tabs %} 6 | {% tab title="Node" %} 7 | ```bash 8 | # Install via NPM 9 | npm install --save @holium/tome-db 10 | 11 | # Install via Yarn 12 | yarn add @holium/tome-db 13 | ``` 14 | {% endtab %} 15 | {% endtabs %} 16 | 17 | Pair with [create-landscape-app](https://github.com/urbit/create-landscape-app) for a bootstrapped React application to build Urbit apps from. 18 | 19 | ## Urbit Setup 20 | 21 | Visit Urbit's [command-line install](https://urbit.org/getting-started/cli) page to install the binary for your system. Afterwards, boot a fresh ship: 22 | 23 | ```hoon 24 | $ ./urbit -F zod 25 | ... this can take a few minutes 26 | 27 | ~zod:dojo> |new-desk %tome 28 | ~zod:dojo> |mount %tome 29 | ``` 30 | 31 | Next, you'll need to copy the Tome desk into your ship: 32 | 33 | ```bash 34 | $ git clone https://github.com/holium/tome-db 35 | $ cp -R tome-db/desk/* zod/tome/ 36 | ``` 37 | 38 | Finally, start the Tome back-end: 39 | 40 | ```hoon 41 | ~zod:dojo> |commit %tome 42 | ~zod:dojo> |revive %tome 43 | ``` 44 | 45 | If you want to develop a spaces-enabled application for [Realm](https://www.holium.com/) (currently in private alpha), you'll also need to configure the ship with Realm's desks. 46 | 47 | ## Basic Usage 48 | 49 | ### Key-value 50 | 51 | ```typescript 52 | const db = await Tome.init(api, 'lexicon', { 53 | realm: true 54 | }) 55 | const kv = await db.keyvalue({ 56 | bucket: 'preferences', 57 | permissions: { read: 'open', write: 'space', admin: 'our' }, 58 | preload: false, 59 | }) 60 | 61 | await kv.set('foo', 'bar') 62 | await kv.set('complex', { foo: ['bar', 3, true] }) 63 | await kv.all() 64 | await kv.remove('foo') 65 | await kv.get('complex') 66 | await kv.clear() 67 | ``` 68 | 69 | ### Feed + Log 70 | 71 | ```typescript 72 | // ... React app boilerplate 73 | const [data, setData] = useState([]) 74 | 75 | const db = await Tome.init(api, 'racket', { 76 | realm: true 77 | }) 78 | // use db.log(...) for a log 79 | const feed = await db.feed({ 80 | preload: true, 81 | permissions: { read: 'space', write: 'space', admin: 'our' }, 82 | onDataChange: (data) => { 83 | // data comes back sorted, newest entries first. 84 | // if you want a different order, you can sort the data here. 85 | setData([...data]) 86 | }, 87 | }) 88 | 89 | const id = await feed.post( 90 | 'https://pbs.twimg.com/media/FmHxG_UX0AACbZY?format=png&name=900x900' 91 | ) 92 | await feed.edit(id, 'new-post-url') 93 | await feed.setLink(id, {reaction: ':smile:', comment: 'amazing!'}) 94 | await feed.delete(id) 95 | ``` 96 | -------------------------------------------------------------------------------- /reference/feed-+-log-api.md: -------------------------------------------------------------------------------- 1 | # Feed + Log API 2 | 3 | ## Types 4 | 5 | ```typescript 6 | // store any valid primitive JS type. 7 | // Maintains type on retrieval. 8 | type Content = string | number | boolean | object | Content[] 9 | 10 | interface FeedlogEntry = { 11 | id: string 12 | createdAt: number 13 | updatedAt: number 14 | createdBy: string 15 | updatedBy: string 16 | content: Content 17 | links: object // authors as keys, Content as values 18 | } 19 | ``` 20 | 21 | ## Feedlog Methods 22 | 23 | ### `post` 24 | 25 | `feedlog.post(content: Content)` 26 | 27 | **Params**: `content` to post to the feedlog. 28 | 29 | **Returns**: A promise resolving to a `string` (the post ID) on success or `undefined` on failure. 30 | 31 | Add a new post to the feedlog. Automatically stores the creation time and author. 32 | 33 | ```typescript 34 | const id = await feedlog.post({ foo: ['bar', 3, true] }) 35 | ``` 36 | 37 | ### `edit` 38 | 39 | `feedlog.edit(id: string, newContent: Content)` 40 | 41 | **Params**: The `id` of the post to edit, and `newContent` to replace it with. 42 | 43 | **Returns**: A promise resolving to a `string` (the post ID) on success or `undefined` on failure. 44 | 45 | Edit a post in the feedlog. Automatically stores the updated time and author. 46 | 47 | ```typescript 48 | const id = await feedlog.post('original post') 49 | if (id) { 50 | await feedlog.edit(id, 'edited post') 51 | } 52 | ``` 53 | 54 | ### `delete` 55 | 56 | `feedlog.delete(id: string)` 57 | 58 | **Params**: The `id` of the post to delete. 59 | 60 | **Returns**: A promise resolving to `true` on success or `false` on failure. 61 | 62 | Delete a post from the feedlog. If the post with `id` does not exist, returns `true`. 63 | 64 | ```typescript 65 | const id = await feedlog.post('original post') 66 | if (id) { 67 | await feedlog.delete(id) 68 | } 69 | ``` 70 | 71 | ### `clear` 72 | 73 | `feedlog.clear()` 74 | 75 | **Returns**: A promise resolving to `true` on success or `false` on failure. 76 | 77 | Clear all posts from the feedlog. 78 | 79 | ```typescript 80 | await feedlog.clear() 81 | ``` 82 | 83 | ### `get` 84 | 85 | `feedlog.get(id: string, allowCachedValue: boolean = true)` 86 | 87 | **Params**: The `id` of the post to retrieve. If `allowCachedValue` is `true`, we will check the cache before querying Urbit. 88 | 89 | **Returns**: A promise resolving to a `FeedlogEntry` or `undefined` if the post does not exist. 90 | 91 | Get the post from the feedlog with the given `id`. 92 | 93 | ```typescript 94 | await feedlog.get('foo') 95 | ``` 96 | 97 | ### `all` 98 | 99 | `feedlog.all(useCache: boolean = false)` 100 | 101 | **Params**: If `useCache` is `true`, return the current cache instead of querying Urbit. Only relevant if `preload` was set to `false`. 102 | 103 | **Returns**: A promise resolving to a `FeedlogEntry[]` 104 | 105 | Retrieve all posts from the feedlog, sorted by newest first. 106 | 107 | ```typescript 108 | await feedlog.all() 109 | ``` 110 | 111 | ## Feed Methods 112 | 113 | ### `setLink` 114 | 115 | `feed.setLink(id: string, content: Content)` 116 | 117 | **Params**: The `id` of the post to link to, and the `content` to associate with it. 118 | 119 | **Returns**: A promise resolving to `true` on success or `false` on failure. 120 | 121 | Associate a "link" (comment, reaction, etc.) with the feed post corresponding to `id`. Post links are stored as an object where keys are ship names and values are content. `setLink` will overwrite the link for the current ship, so call `get` first if you would like to append. 122 | 123 | ```typescript 124 | const id = await feedlog.post('original post') 125 | if (id) { 126 | await feed.setLink(id, { comment: 'first comment!', time: Date.now() }) 127 | } 128 | ``` 129 | 130 | ### `removeLink` 131 | 132 | `feed.removeLink(id: string)` 133 | 134 | **Params**: The `id` of the post to remove the link from. 135 | 136 | **Returns**: A promise resolving to `true` on success or `false` on failure. 137 | 138 | Remove the current ship's link to the feed post corresponding to `id`. If the post with `id` does not exist, returns true. 139 | 140 | ```typescript 141 | const id = await feedlog.post('original post') 142 | if (id) { 143 | await feed.setLink(id, { comment: 'first comment!', time: Date.now() }) 144 | await feed.removeLink(id) 145 | } 146 | ``` 147 | -------------------------------------------------------------------------------- /reference/initializing-stores.md: -------------------------------------------------------------------------------- 1 | # Initializing Stores 2 | 3 | ## Permissions 4 | 5 | It's recommended to specify `permissions` on either `Tome` or its subclasses. The schema is: 6 | 7 | ```typescript 8 | interface Perm { 9 | read: 'our' | 'space' | 'open' 10 | write: 'our' | 'space' | 'open' 11 | admin: 'our' | 'space' | 'open' 12 | } 13 | ``` 14 | 15 | * `our` is our ship only (`src.bowl`) 16 | * `space` is any ship in the current Holium space. If Realm is not installed, this becomes `our`. 17 | * `open` is anyone, including comets. 18 | 19 | `read` / `write` / `admin` mean slightly different things based on the store type, so they will be described below. 20 | 21 | ## `Tome` 22 | 23 | `Tome.init(api?: Urbit, app?: string, options?)` 24 | 25 | * `api`: The optional Urbit connection to be used for requests. If not set, TomeDB will attempt to use [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) for storing key-value pairs. 26 | * `app`: An optional app name to store under. Defaults to `'all'`. It's recommended to set this to the name of your application (be wary of collisions, though!) 27 | 28 |
29 | 30 | options 31 | 32 | Optional `ship`, `space`, `agent`, `permissions`, and `realm` flag for initializing a Tome. 33 | 34 | ```typescript 35 | options: { 36 | realm?: boolean = false 37 | ship?: string = api.ship 38 | space?: string = 'our' 39 | agent?: string = 'tome' 40 | permissions?: Perm 41 | } 42 | ``` 43 | 44 | * `ship` can be specified with or without the `~`. 45 | * `agent` specifies both the desk and agent name that the JavaScript client will use. This is useful if you want to distribute a copy of TomeDB in the desk alongside your application. 46 | * If `realm` is `false` , Tome will use `ship` and `space` as specified. 47 | * If `realm` is `true`, `ship` and `space` must be either set together or not at all. 48 | * If neither are set, Tome will automatically detect and use the current Realm space and corresponding host ship, as well as handle switching application data when a user changes spaces in Realm. 49 | * To create a "locked" Tome, specify `ship` and `space` together. A locked Tome will work only in that specific space (think internal DAO tooling). 50 | 51 | 52 | 53 | * `permissions` is a default permissions level to be used by sub-classes. When creating many store instances with the same permissions, simply specify them once here. 54 | 55 |
56 | 57 | **Returns**: `Promise` 58 | 59 | All storage types must be created from a `Tome` instance, so do this first. 60 | 61 | ```typescript 62 | const api = new Urbit('', '', window.desk) 63 | api.ship = window.ship 64 | 65 | const db = await Tome.init(api, 'demo', { 66 | realm: true, 67 | ship: 'lomder-librun', 68 | space: 'Realm Forerunners', 69 | agent: 'tome', 70 | permissions: { read: 'space', write: 'space', admin: 'our' } 71 | }) 72 | ``` 73 | 74 | ## `Key-value` 75 | 76 | `db.keyvalue(options?)` 77 | 78 |
79 | 80 | options 81 | 82 | Optional `bucket`, `permissions`, `preload` flag, and callbacks for the key-value store. 83 | 84 | ```typescript 85 | options: { 86 | bucket?: string = 'def' 87 | preload?: boolean = true 88 | permissions?: Perm 89 | onDataChange?: (data: Map()) => void 90 | onLoadChange?: (loaded: boolean) => void 91 | onReadyChange?: (ready: boolean) => void 92 | onWriteChange?: (write: boolean) => void 93 | onAdminChange?: (admin: boolean) => void 94 | } 95 | ``` 96 | 97 | * `bucket` is the bucket name to store key-value pairs under. If your app needs multiple key-value stores with different permissions, they should be different buckets. Separating buckets can also save on download sizes depending on the application. 98 | * `preload` is whether the client should fetch and cache all key-value pairs in the bucket, and subscribe to live updates. This helps with responsiveness when using an application, since most requests won't go to Urbit. 99 | * `permissions` is the permissions for the key-value store. If not set, defaults to the Tome-level permissions. 100 | * `read` can read any key-value pairs from the bucket. 101 | * `write` can create new key-value pairs or update their own values. 102 | * `admin` can create or overwrite any values in the bucket. 103 | * `onDataChange` is called whenever data in the key-value store changes, and can be used to re-render an application with new data. 104 | * `onReadyChange` is called whenever the store changes `ready` state: after initial app configuration, and whenever a user changes between spaces in Realm. Use combined with `preload` set to `false` to know when to show a loading screen, and when to start making requests. 105 | * If preload is `true`, use `onLoadChange` instead to be notified when all data has been loaded and is addressable. This also handles the case of switching between Realm spaces. 106 | * `onWriteChange` and `onAdminChange` are called when the current user's `write` and `admin` permissions have been detected to change. 107 | 108 |
109 | 110 | **Returns**: `Promise` 111 | 112 | Initialize or connect to a key-value store. It uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) if the corresponding `Tome` has no Urbit connection. 113 | 114 | ```typescript 115 | const db = await Tome.init(api, 'demo', { 116 | realm: true 117 | }) 118 | 119 | const kv = await db.keyvalue({ 120 | bucket: 'preferences', 121 | permissions: { read: 'space', write: 'space', admin: 'our' }, 122 | preload: true, 123 | onDataChange: setData, 124 | onReadyChange: setReady, 125 | onWriteChange: setWrite, 126 | onAdminChange: setAdmin 127 | }) 128 | ``` 129 | 130 | {% content-ref url="key-value-api.md" %} 131 | [key-value-api.md](key-value-api.md) 132 | {% endcontent-ref %} 133 | 134 | ## `Feed + Log` 135 | 136 | `db.feed(options?)` or `db.log(options?)` 137 | 138 |
139 | 140 | options 141 | 142 | Optional `bucket`, `permissions`, `preload` flag, and callbacks for the log or feed store. 143 | 144 | ```typescript 145 | options: { 146 | bucket?: string = 'def' 147 | preload?: boolean = true 148 | permissions?: Perm 149 | onDataChange?: (data: FeedlogEntry[]) => void 150 | onLoadChange?: (loaded: boolean) => void 151 | onReadyChange?: (ready: boolean) => void 152 | onWriteChange?: (write: boolean) => void 153 | onAdminChange?: (admin: boolean) => void 154 | } 155 | ``` 156 | 157 | `permissions` is the permissions for the feedlog store. If not set, defaults to the Tome-level permissions. 158 | 159 | * `read` can read any posts and metadata from the bucket. 160 | * For a `feed`, `write` can create or update their own posts / links. For a `log`, `write` only allows creating new posts. 161 | * `admin` can create or overwrite any posts / links in the bucket. 162 | 163 | Refer to the options under [key-value](initializing-stores.md#key-value) for more information, as the rest are functionally identical. 164 | 165 |
166 | 167 | **Returns**: `Promise` or `Promise` 168 | 169 | Initialize a feed or log store. These must be created from a `Tome` with a valid Urbit connection. 170 | 171 | {% hint style="info" %} 172 | `feed` and `log` are very similar, the only differences are: 173 | 174 | * `write` for a log can't update any values. This makes a log more similar to an "append-only" data structure. 175 | * "Links" (comments, reactions, etc.) currently aren't supported for logs but are for feeds. 176 | {% endhint %} 177 | 178 | ```typescript 179 | const db = await Tome.init(api, 'demo', { 180 | realm: true 181 | }) 182 | 183 | const feed = await db.feed({ 184 | bucket: 'posts', 185 | preload: true, 186 | permissions: { read: 'space', write: 'space', admin: 'our' }, 187 | onLoadChange: setLoaded, 188 | onDataChange: (data) => { 189 | // newest records first. 190 | // if you want a different order, you can sort the data here. 191 | // need to spread array to trigger re-render 192 | setData([...data]) 193 | }, 194 | onWriteChange: setWrite, 195 | onAdminChange: setAdmin 196 | }) 197 | ``` 198 | 199 | {% content-ref url="feed-+-log-api.md" %} 200 | [feed-+-log-api.md](feed-+-log-api.md) 201 | {% endcontent-ref %} 202 | -------------------------------------------------------------------------------- /reference/key-value-api.md: -------------------------------------------------------------------------------- 1 | # Key-value API 2 | 3 | ## Types 4 | 5 | ```typescript 6 | // store any valid primitive JS type. 7 | // Maintains type on retrieval. 8 | type Value = string | number | boolean | object | Value[] 9 | ``` 10 | 11 | ## Methods 12 | 13 | ### `set` 14 | 15 | `kv.set(key: string, value: Value)` 16 | 17 | **Params**: a `key` and corresponding `value`. 18 | 19 | **Returns**: A promise resolving to `true` on success or `false` on failure. 20 | 21 | Set a key-value pair in the store. Overwrites existing values. 22 | 23 | ```typescript 24 | await kv.set('complex', { foo: ['bar', 3, true] }) 25 | ``` 26 | 27 | ### `get` 28 | 29 | `kv.get(key: string, allowCachedValue: boolean = true)` 30 | 31 | **Params**: A `key` to retrieve. If `allowCachedValue` is `true`, we will check the cache before querying Urbit. 32 | 33 | **Returns**: A promise resolving to a `Value` or `undefined` if the key does not exist. 34 | 35 | Get the value associated with a key in the store. 36 | 37 | ```typescript 38 | await kv.get('foo') 39 | ``` 40 | 41 | ### `remove` 42 | 43 | `kv.remove(key: string)` 44 | 45 | **Params**: a `key` to remove. 46 | 47 | **Returns**: A promise resolving to `true` on success or `false` on failure. 48 | 49 | Remove a key-value pair from the store. If the `key` does not exist, returns `true`. 50 | 51 | ```typescript 52 | await kv.remove('foo') 53 | ``` 54 | 55 | ### `clear` 56 | 57 | `kv.clear()` 58 | 59 | **Returns**: A promise resolving to `true` on success or `false` on failure. 60 | 61 | Clear all key-value pairs from the store. 62 | 63 | ```typescript 64 | await kv.clear() 65 | ``` 66 | 67 | ### `all` 68 | 69 | `kv.all(useCache: boolean = false)` 70 | 71 | **Params**: If `useCache` is `true`, return the current cache instead of querying Urbit. Only relevant if `preload` was set to `false`. 72 | 73 | **Returns**: A promise resolving to a `Map>` 74 | 75 | Get all key-value pairs in the store. 76 | 77 | ```typescript 78 | await kv.all() 79 | ``` 80 | -------------------------------------------------------------------------------- /reference/managing-permissions.md: -------------------------------------------------------------------------------- 1 | # Managing Permissions 2 | 3 | ## Types 4 | 5 | ```typescript 6 | type InviteLevel = 'read' | 'write' | 'admin' | 'block' 7 | 8 | // 'admin' allows read + write + admin 9 | // 'write' allows read + write 10 | // 'read' allows only read 11 | ``` 12 | 13 | ## Methods 14 | 15 | _These can only be done by the Tome owner._ 16 | 17 | ### `setPermissions` 18 | 19 | `store.setPermissions(permissions: Perm)` 20 | 21 | **Params**: the new `permissions` to set. 22 | 23 | **Returns**: `Promise` 24 | 25 | Update a store's permissions after initialization. 26 | 27 | ```typescript 28 | // store is one of: KeyValueStore, LogStore, or FeedStore 29 | await store.setPermissions({ 30 | read: 'our', 31 | write: 'our' 32 | admin: 'our' 33 | }) 34 | ``` 35 | 36 | ### `inviteShip` 37 | 38 | `store.inviteShip(ship: string, level: InviteLevel)` 39 | 40 | **Params**: the `ship` to set permissions for and the `level` to set to. 41 | 42 | **Returns**: `Promise` 43 | 44 | Set permissions for a specific ship. Invites take precedence over bucket-level permissions. 45 | 46 | ```typescript 47 | // store is one of: KeyValueStore, LogStore, or FeedStore 48 | await store.inviteShip('timluc-miptev', 'admin') 49 | ``` 50 | 51 | ### `blockShip` 52 | 53 | `store.blockShip(ship: string)` 54 | 55 | **Params**: the `ship` to block. 56 | 57 | **Returns**: `Promise` 58 | 59 | An alias for `inviteShip(ship, 'block')`. 60 | 61 | ```typescript 62 | await store.blockShip('sorreg-namtyv') 63 | ``` 64 | --------------------------------------------------------------------------------