├── .editorconfig ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── LICENSE.md ├── README.md ├── VERSIONS.md ├── angular.json ├── commitlint.config.js ├── deploy-doc.js ├── logo ├── logo-for-export.svg ├── logo.svg ├── optimized-svg │ └── logo-for-export.svg └── slicelogo.png ├── package-lock.json ├── package.json ├── projects └── slice │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── AbstractStore.ts │ │ ├── EStore.spec.ts │ │ ├── EStore.ts │ │ ├── OStore.spec.ts │ │ ├── OStore.ts │ │ ├── Slice.spec.ts │ │ ├── Slice.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── ActionTypes.ts │ │ │ ├── Delta.ts │ │ │ ├── Entity.ts │ │ │ ├── Predicate.ts │ │ │ ├── StoreConfig.ts │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── scrollPosition.ts │ │ ├── test-setup.ts │ │ ├── utilities.spec.ts │ │ └── utilities.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── slicelogo.png └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Typedoc output 4 | /doc 5 | # Compiled output 6 | /dist 7 | /tmp 8 | /out-tsc 9 | /bazel-out 10 | 11 | # Node 12 | /node_modules 13 | npm-debug.log 14 | yarn-error.log 15 | 16 | # IDEs and editors 17 | .idea/ 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # Visual Studio Code 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # Miscellaneous 34 | /.angular/cache 35 | .sass-cache/ 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | testem.log 40 | /typings 41 | 42 | # System files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | ng test --watch=false --browsers=ChromeHeadless 2 | npx lint-staged -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Firefly Semantics Corporation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Slice](slicelogo.png) 2 | 3 | # @fireflysemantics/slice 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [Why Slice](#why-slice) 9 | - [Install](#install) 10 | - [API Reference](#api-reference) 11 | - [Object Store Core Use Cases](#object-store-core-use-cases) 12 | - [Entity Store Core Use Cases](#entity-store-core-use-cases) 13 | - [Features](#features) 14 | - [Help Center Documentation and Media](#firefly-semantics-slice-development-center-media-and-documentation) 15 | - [Getting Help](#getting-help) 16 | 17 | ## Overview 18 | 19 | Lightweight Javascript Reactive State Management for Angular Applications. 20 | 21 | The API is designed to be as minimal as possible and should deliver the same features as other comparable frameworks with about 1/3 the lines of code. 22 | 23 | It offers two types of reactive data stores: 24 | - Entity stores (EStore) for structured entity like data (Customer, Product, User, ...). Entity stores can be "Live filtered" by adding slices. For example separating Todo entities into complete and incomplete compartments. Slices are also obserable. 25 | - Object store (Key value store) for unstructured data 26 | 27 | Even though Angular is used for prototype applications, it should work well in general for: 28 | - Single page applications 29 | - Progressive web applications 30 | - Node applications / Pure Javascript Applications 31 | - Mobile Applications 32 | 33 | If you like the [@fireflysemantics/slice API](https://fireflysemantics.github.io/slice/doc/) please star our [Github Repository](https://github.com/fireflysemantics/slice). 34 | 35 | # Why Slice 36 | 37 | We built Slice to make sharing state between Angular components, services, and other directives simple and in the process we targeted common use cases that should be handled by a state manager, such as updating a shopping cart count, emitting a search query, tracking active state, etc. 38 | 39 | For performing state CRUD operations Slice uses a REST like API, which should be a familiar paradigm for most developers. 40 | 41 | For example a `Todo` entity store tracking todo entities can create, read, update, and delete `Todo` entities as follows (This is just a tiny example of all the capabilities Slice has). 42 | 43 | ``` 44 | let store: EStore = new EStore(); 45 | //============================================ 46 | // Post (Create) a Todo instance in the store 47 | //============================================ 48 | store.post(todo); 49 | //============================================ 50 | // Snapshot of all the Todo entities in the 51 | // store 52 | //============================================ 53 | let snapshot: Todo[] = store.allSnapshot(); 54 | //============================================ 55 | // Observe the array of Todo instances in the 56 | // store 57 | //============================================ 58 | store.obs.subscribe((todos: Todo[]) => { 59 | console.log(`The store is initialized with these Todo entities ${todos}`); 60 | }); 61 | //============================================ 62 | // Delete a Todo instance in the store 63 | //============================================ 64 | todo.complete = false; 65 | store.put(todo); 66 | //============================================ 67 | // Delete a Todo instance in the store 68 | //============================================ 69 | store.delete(todo); 70 | ``` 71 | 72 | # Install 73 | 74 | 75 | Install Slice with the `nanoid` peer dependency: 76 | 77 | - `v17.0.x` for Angular 17 78 | - `v16.2.x` for Angular 16 79 | - `v15.2.x` for Angular 15 80 | 81 | So for example for an Angular 15 project run. 82 | 83 | ``` 84 | npm i @fireflysemantics/slice@15.2.x nanoid 85 | ``` 86 | For Angular 17 run. 87 | 88 | ``` 89 | npm i @fireflysemantics/slice@lastest nanoid 90 | ``` 91 | 92 | # Usage 93 | 94 | The project [typedoc](https://fireflysemantics.github.io/slice/typedoc/), in addition to providing more detailed API insight, syntax highlights all the examples provided here, thus you may want to check it out for a richer reading experience. 95 | 96 | ## Object Store Core Use Cases 97 | 98 | [Here is a link to the Stackblitz Demo](https://stackblitz.com/edit/typescript-9snt67?file=index.ts) 99 | containing all of the below examples. 100 | 101 | In this demo we are using simple `string` values, but we could have used objects or essentially anything that can be referenced by Javascript. 102 | 103 | ``` 104 | import { 105 | KeyObsValueReset, 106 | ObsValueReset, 107 | OStore, 108 | OStoreStart, 109 | } from '@fireflysemantics/slice'; 110 | const START: OStoreStart = { 111 | K1: { value: 'V1', reset: 'ResetValue' }, 112 | }; 113 | interface ISTART extends KeyObsValueReset { 114 | K1: ObsValueReset; 115 | } 116 | let OS: OStore = new OStore(START); 117 | 118 | //============================================ 119 | // Log a snapshot the initial store value. 120 | // This will log 121 | // V1 122 | //============================================ 123 | const v1Snapshot: string = OS.snapshot(OS.S.K1); 124 | console.log(`The value for the K1 key is ${v1Snapshot}`); 125 | 126 | //============================================ 127 | // Observe the initial store value. 128 | // The subsription will log 129 | // V1 130 | //============================================ 131 | OS.S.K1.obs.subscribe((v) => console.log(`The subscribed to value is ${v}`)); 132 | 133 | //============================================ 134 | // Update the initial store value 135 | // The subsription will log 136 | // New Value 137 | //============================================ 138 | OS.put(OS.S.K1, 'New Value'); 139 | 140 | //============================================ 141 | // Log a count of the number of entries in the 142 | // object store. 143 | // This will log 144 | // 1 145 | //============================================ 146 | const count: number = OS.count(); 147 | console.log( 148 | `The count of the number of entries in the Object Store is ${count}` 149 | ); 150 | 151 | //============================================ 152 | // Reset the store 153 | // The subsription will log 154 | // ResetValue 155 | // 156 | // However if we had not specified a reset 157 | // value it would have logged in value 158 | // V1 159 | //============================================ 160 | OS.reset(); 161 | 162 | //============================================ 163 | // Delete the K1 entry 164 | // The subsription will log and the snapshot 165 | // will also be 166 | // undefined 167 | //============================================ 168 | OS.delete(OS.S.K1); 169 | const snapshot: string = OS.snapshot(OS.S.K1); 170 | console.log(`The deleted value snapshot for the K1 key is ${snapshot}`); 171 | 172 | //============================================ 173 | // Clear the store. First we will put a new 174 | // value back in the store to demonstrate it 175 | // being cleared. 176 | //============================================ 177 | //============================================ 178 | // Update the initial store value 179 | // The subsription will log 180 | // New Value 181 | //============================================ 182 | OS.put(OS.S.K1, 'V2'); 183 | 184 | OS.clear(); 185 | //============================================ 186 | // Count the number of values in the store 187 | // It will be zero. 188 | // The OS.clear() call will remove all the 189 | // entries and so the snapshot will be undefined 190 | // and the subscribed to value also undefined. 191 | // The count will be zero. 192 | //============================================ 193 | console.log(`The count is ${OS.count()}`); 194 | console.log(`The snapshot is ${OS.snapshot(OS.S.K1)}`); 195 | ``` 196 | 197 | ## Entity Store Core Use Cases 198 | 199 | [Here is a link to the Stackblitz demo](https://stackblitz.com/edit/typescript-wayluo?file=index.ts) containing the below demo code. You may also wish to check out the [test cases](https://github.com/fireflysemantics/slice/blob/master/projects/slice/src/lib/EStore.spec.ts) for the entity store which also detail usage scenarios. 200 | 201 | ``` 202 | //============================================ 203 | // Demo Utilities 204 | //============================================ 205 | 206 | export const enum TodoSliceEnum { 207 | COMPLETE = 'Complete', 208 | INCOMPLETE = 'Incomplete', 209 | } 210 | 211 | export class Todo { 212 | constructor( 213 | public complete: boolean, 214 | public title: string, 215 | public gid?: string, 216 | public id?: string 217 | ) {} 218 | } 219 | 220 | export const extraTodo: Todo = new Todo(false, 'Do me later.'); 221 | 222 | export let todos = [ 223 | new Todo(false, 'You complete me!'), 224 | new Todo(true, 'You completed me!'), 225 | ]; 226 | 227 | export function todosFactory(): Todo[] { 228 | return [ 229 | new Todo(false, 'You complete me!'), 230 | new Todo(true, 'You completed me!'), 231 | ]; 232 | } 233 | 234 | export function todosClone(): Todo[] { 235 | return todos.map((obj) => ({ ...obj })); 236 | } 237 | 238 | //============================================ 239 | // API: constructor() 240 | // 241 | // Create a Todo Entity Store 242 | //============================================ 243 | let store: EStore = new EStore(todosFactory()); 244 | 245 | //============================================ 246 | // API: post, put, delete 247 | // 248 | // Perform post (Create), put (Update), and delete opeartions 249 | // on the store. 250 | //============================================ 251 | const todoLater: Todo = new Todo(false, 'Do me later.'); 252 | todoLater.id = 'findMe'; 253 | store.post(todoLater); 254 | const postedTodo = store.findOneByID('findMe'); 255 | postedTodo.title = 'Do me sooner'; 256 | store.put(postedTodo); 257 | store.delete(postedTodo); 258 | 259 | //============================================ 260 | // API: allSnapshot() 261 | // 262 | // Take a snapshot of all the entities 263 | // in the store 264 | //============================================ 265 | let snapshot: Todo[] = store.allSnapshot(); 266 | 267 | //============================================ 268 | // API: obs 269 | // 270 | // Create a subscription to the entities in 271 | // the store. 272 | //============================================ 273 | let todosSubscription: Subscription = store.obs.subscribe((todos: Todo[]) => { 274 | console.log(`The store todos ${todos}`); 275 | }); 276 | 277 | //============================================ 278 | // API: findOne() 279 | // 280 | // Find a Todo instance using the 281 | // Global ID (guid) property. 282 | //============================================ 283 | const globalID: string = '1'; 284 | let findThisTodo = new Todo(false, 'Find this Todo', globalID); 285 | 286 | store.post(findThisTodo); 287 | 288 | const todo = store.findOne(globalID); 289 | console.log(todo); 290 | 291 | //============================================ 292 | // API: findOneByID() 293 | // 294 | // Find a Todo instance using the 295 | // ID (id) property. 296 | //============================================ 297 | const ID: string = 'id'; 298 | let todoWithID = new Todo(false, 'Find this Todo by ID'); 299 | todoWithID.id = ID; 300 | 301 | store.post(todoWithID); 302 | const todoFoundByID = store.findOneByID(ID); 303 | 304 | console.log(`The Todo instance found by id is ${todoFoundByID}`); 305 | 306 | //============================================ 307 | // API: select() 308 | // 309 | // Select Todo instances where the title 310 | // includes the string Find. 311 | //============================================ 312 | const selectLaterPredicate: Predicate = (todo: Todo) => { 313 | return todo.title.includes('Find'); 314 | }; 315 | const selections = store.select(selectLaterPredicate); 316 | console.log( 317 | `The selected todo instances that contain Find are: ${selections.length}` 318 | ); 319 | 320 | //============================================ 321 | // API: observeLoading() 322 | // 323 | // Subscribe to the store loading indicator 324 | // and toggle it to see the values change. 325 | //============================================ 326 | store.observeLoading().subscribe((loading) => { 327 | console.log(`Is data loading: ${loading}`); 328 | }); 329 | store.loading = true; 330 | store.loading = false; 331 | 332 | //============================================ 333 | // API: observeSearching() 334 | // 335 | // Subscribe to the store searching indicator 336 | // and toggle it to see the values change. 337 | //============================================ 338 | store.observeSearching().subscribe((searching) => { 339 | console.log(`Is the store searching: ${searching}`); 340 | }); 341 | store.searching = true; 342 | store.searching = false; 343 | 344 | //============================================ 345 | // API: addActive() 346 | // Perform active state tracking. Initially the 347 | // number of active entities will be zero. 348 | //============================================ 349 | console.log(`The number of active Todo instances is ${store.active.size}`); 350 | let todo1: Todo = new Todo(false, 'The first Todo!', GUID()); 351 | let todo2: Todo = new Todo(false, 'The first Todo!', GUID()); 352 | store.addActive(todo1); 353 | console.log(`The number of active Todo instances is ${store.active.size}`); 354 | 355 | console.log( 356 | `The number of active Todo instances by the activeSnapshot is ${ 357 | store.activeSnapshot().length 358 | }` 359 | ); 360 | 361 | //============================================ 362 | // API: observeActive() 363 | // 364 | // Subscribing to the observeActive() observable 365 | // provides the map of active Todo instances. 366 | //============================================ 367 | store.observeActive().subscribe((active) => { 368 | console.log(`The active Todo instances are: ${active}`); 369 | }); 370 | 371 | //============================================ 372 | // API: deleteActive() 373 | // Delete the active Todo instance. 374 | // This will set the number of active 375 | // Todo instances back to zero. 376 | //============================================ 377 | store.deleteActive(todo1); 378 | console.log( 379 | `The number of active Todo instances by the activeSnapshot is ${ 380 | store.activeSnapshot().length 381 | }` 382 | ); 383 | 384 | //============================================ 385 | // API: count() and snapshotCount() 386 | // 387 | // Take snapshot and observable 388 | // the counts of store entities 389 | //============================================ 390 | 391 | const completePredicate: Predicate = function pred(t: Todo) { 392 | return t.complete; 393 | }; 394 | 395 | const incompletePredicate: Predicate = function pred(t: Todo) { 396 | return !t.complete; 397 | }; 398 | 399 | store.count().subscribe((c) => { 400 | console.log(`The observed count of Todo entities is ${c}`); 401 | }); 402 | store.count(incompletePredicate).subscribe((c) => { 403 | console.log(`The observed count of incomplete Todo enttiies is ${c}`); 404 | }); 405 | store.count(completePredicate).subscribe((c) => { 406 | console.log(`The observed count of complete Todo enttiies is ${c}`); 407 | }); 408 | 409 | const snapshotCount = store.countSnapshot(completePredicate); 410 | console.log(`The count is ${snapshotCount}`); 411 | 412 | const completeSnapshotCount = store.countSnapshot(completePredicate); 413 | console.log( 414 | `The complete Todo Entity Snapshot count is ${completeSnapshotCount}` 415 | ); 416 | 417 | const incompleteSnapshotCount = store.countSnapshot(incompletePredicate); 418 | console.log( 419 | `The incomplete Todo Entity Snapshot count is ${incompleteSnapshotCount}` 420 | ); 421 | 422 | 423 | //============================================ 424 | // API: toggle() 425 | // 426 | // When we post another todo using toggle 427 | // instance the subscribed to count 428 | // dynamically increases by 1. 429 | // When we call toggle again, 430 | // removing the instance the 431 | // count decreases by 1. 432 | //============================================ 433 | store.toggle(extraTodo); 434 | store.toggle(extraTodo); 435 | 436 | //============================================ 437 | // API: contains() 438 | // 439 | // When we post another todo using toggle 440 | // the store now contains it. 441 | //============================================ 442 | console.log( 443 | `Does the store contain the extraTodo ${store.contains(extraTodo)}` 444 | ); 445 | store.toggle(extraTodo); 446 | console.log( 447 | `Does the store contain the extraTodo ${store.contains(extraTodo)}` 448 | ); 449 | store.toggle(extraTodo); 450 | console.log( 451 | `Does the store contain the extraTodo ${store.contains(extraTodo)}` 452 | ); 453 | 454 | //============================================ 455 | // API: containsbyID() 456 | // 457 | // When we post another todo using toggle 458 | // the store now contains it. 459 | // 460 | // Note the containsByID() can be called with 461 | // both the id property or the entire instance. 462 | //============================================ 463 | let todoByID = new Todo(false, 'This is not in the store', undefined, '1'); 464 | store.post(todoByID); 465 | console.log( 466 | `Does the store contain the todoByID ${store.containsById(todoByID.id)}` 467 | ); 468 | console.log( 469 | `Does the store contain the todoByID ${store.containsById(todoByID)}` 470 | ); 471 | store.toggle(todoByID); 472 | console.log( 473 | `Does the store contain the todoByID ${store.containsById(todoByID.id)}` 474 | ); 475 | console.log( 476 | `Does the store contain the todoByID ${store.containsById(todoByID)}` 477 | ); 478 | 479 | //============================================ 480 | // API: equalsByGUID and equalsByID 481 | // 482 | // Compare entities by ID and Global ID (guid). 483 | // We will assign the ID and the global ID 484 | // instead of allowing the global ID to be 485 | // assigned by the store on post. 486 | //============================================ 487 | const guid = GUID(); 488 | let todoOrNotTodo1 = new Todo(false, 'Apples to Apples', guid, '1'); 489 | let todoOrNotTodo2 = new Todo(false, 'Apples to Apples', guid, '1'); 490 | 491 | const equalByID: boolean = store.equalsByID(todoOrNotTodo1, todoOrNotTodo2); 492 | console.log(`Are the todos equal by id: ${equalByID}`); 493 | const equalByGUID: boolean = store.equalsByGUID(todoOrNotTodo1, todoOrNotTodo2); 494 | console.log(`Are the todos equal by global id: ${equalByGUID}`); 495 | 496 | //============================================ 497 | // API: addSlice 498 | // 499 | // Add a slice for complete todo entities. 500 | // 501 | // We create a new store to demo with a 502 | // consistent count. 503 | // 504 | // When posting the extraTodo which is 505 | // incomplete, we see that the incomplete 506 | // count increments. 507 | //============================================ 508 | store.destroy(); 509 | store = new EStore(todosFactory()); 510 | store.addSlice((todo) => todo.complete, TodoSliceEnum.COMPLETE); 511 | store.addSlice((todo) => !todo.complete, TodoSliceEnum.INCOMPLETE); 512 | const completeSlice = store.getSlice(TodoSliceEnum.COMPLETE); 513 | const incompleteSlice = store.getSlice(TodoSliceEnum.INCOMPLETE); 514 | completeSlice.count().subscribe((c) => { 515 | console.log(`The number of entries in the complete slice is ${c}`); 516 | }); 517 | incompleteSlice.count().subscribe((c) => { 518 | console.log(`The number of entries in the incomplete slice is ${c}`); 519 | }); 520 | store.post(extraTodo); 521 | const incompleteTodos: Todo[] = incompleteSlice.allSnapshot(); 522 | console.log(`The incomplete Todo entities are ${incompleteTodos}`); 523 | 524 | //============================================ 525 | // API: isEmpty() 526 | // 527 | // Check whether the store is empty. 528 | //============================================ 529 | store.isEmpty().subscribe((empty) => { 530 | console.log(`Is the store empty? ${empty}`); 531 | }); 532 | ``` 533 | 534 | ## Features 535 | 536 | - Live Stackblitz demoes 537 | - [Typedoc with inlined examples](https://fireflysemantics.github.io/slice/typedoc/) 538 | - [Well documented test cases run with Jest - Each file has a corresponding `.spec` file](https://github.com/fireflysemantics/slice/tree/master/src) 539 | - Stream both Entity and Object Stores for UI Updates via RxJS 540 | - Define entities using Typescript classes, interfaces, or types 541 | - [Active state tracking](https://medium.com/@ole.ersoy/monitoring-the-currently-active-entity-with-slice-ff7c9b7826e8) 542 | - [Supports for Optimistic User Interfaces](https://medium.com/@ole.ersoy/optimistic-user-identity-management-with-slice-a2b66efe780c) 543 | - RESTful API for performing CRUD operations that stream both full and delta updates 544 | - Dynamic creation of both object and entity stores 545 | - Observable delta updates for Entities 546 | - Real time application of Slice `Predicate` filtering that is `Observable` 547 | - `Predicate` based snapshots of entities 548 | - Observable `count` of entities in the entity store. The `count` feature can also be `Predicate` filtered. 549 | - Configurable global id (Client side id - `gid`) and server id (`id`) id property names for entities. 550 | - The stream of entities can be sorted via an optional boolean expression passed to `observe`. 551 | 552 | # Firefly Semantics Slice Development Center Media and Documentation 553 | 554 | ## Concepts 555 | 556 | - [What is Reactive State Management](https://developer.fireflysemantics.com/concepts/concepts--slice--what-is-reactive-state-management) 557 | 558 | ## Guides 559 | 560 | - [An Introduction to the Firefly Semantics Slice Reactive Object Store](https://developer.fireflysemantics.com/guides/guides--introduction-to-the-firefly-semantics-slice-reactive-object-store) 561 | - [Introduction to the Firefly Semantics Slice Reactive Entity Store ](https://developer.fireflysemantics.com/guides/guides--introduction-to-the-firefly-semantics-slice-reactive-entity-store) 562 | - [Creating a Reactive Todo Application With the Firefly Semantics Slice State Manager](https://developer.fireflysemantics.com/guides/guides--slice--creating-a-reactive-todo-application-with-the-firefly-semantics-slice-state-manager) 563 | - [Recreating the Ngrx Demo with Slice](https://developer.fireflysemantics.com/guides/guides--recreating-the-ngrx-demo-app-with-firefly-semantics-slice-state-manager) 564 | - [Firefly Semantics Slice Entity Store Active API Guide](https://developer.fireflysemantics.com/guides/guides--slice--managing-active-entities-with-firefly-semantics-slice) 565 | 566 | 567 | ## Tasks 568 | 569 | - [Creating a Minimal Slice Object Store](https://developer.fireflysemantics.com/examples/examples--slice--minimal-slice-object-store) 570 | - [Creating a Minimal Angular Slice Object Store Angular State Service ](https://developer.fireflysemantics.com/examples/examples--slice--minial-angular-slice-object-store-state-service) 571 | - [Changing the Firefly Semantics Slice EStore Default Configuration](https://developer.fireflysemantics.com/tasks/tasks--slice--changing-the-fireflysemantics-slice-estore-default-configuration) 572 | - [Observing the Currently Active Entities with Slice](https://developer.fireflysemantics.com/tasks/tasks--slice--observing-currently-active-entities-with-slice) 573 | - [Derived Reactive Observable State with Slice](https://developer.fireflysemantics.com/tasks/tasks--slice--derived-reactive-observable-state-with-slice) 574 | - [Reactive Event Driven Actions with Firefly Semantics Slice](https://developer.fireflysemantics.com/tasks/tasks--slice--reactive-event-driven-actions-with-firefly-semantics-slice) 575 | - [Unsubscribing From Firefly Semantics Slice Object Store Observables in Angular](https://developer.fireflysemantics.com/tasks/tasks--slice--unsubscribing-from-firefly-semantics-slice-object-store-observables-in-angular) 576 | - [Creating Proxies to Slice Object Store Observables](https://developer.fireflysemantics.com/tasks/tasks--slice--creating-proxies-to-slice-object-store-observables) 577 | - [Getting a Snapshot of a Slice Object Store Value](https://developer.fireflysemantics.com/tasks/tasks--slice--getting-a-snapshot-of-a-slice-object-store-value) 578 | - [Accessing Slice Object Store Observables In Angular Templates](https://developer.fireflysemantics.com/tasks/tasks--slice--accessing-slice-object-store-observables-in-angular-templates) 579 | - [Observing the Count of Items in a Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--observing-the-count-of-items-in-a-firefly-semantics-slice-entity-store) 580 | - [Setting and Observing Firefly Semantics Slice Entity Store Queries](https://developer.fireflysemantics.com/tasks/tasks--slice--setting-and-observing-firefly-semantics-slice-entity-store-queries) 581 | - [Taking a Count Snapshot of a Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--taking-a-count-snapshot-of-a-firefly-semantics-slice-entity-store) 582 | - [Taking a Query Snapshot of a Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--taking-a-query-snapshot-of-a-firefly-semantics-slice-entity-store) 583 | - [Adding Slices to an Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--adding-slices-to-the-firefly-semantics-entity-store) 584 | - [Adding Search To the Firefly Semantics Slice Angular Todo Application](https://developer.fireflysemantics.com/tasks/tasks--slice--adding-search-to-the-firefly-semantics-slice-angular-todo-application) 585 | - [Comparing Firefly Semantics Slice Entities](https://developer.fireflysemantics.com/tasks/tasks--slice--comparing-firefly-semantics-slice-entities) 586 | - [Filtering Firefly Semantics Slice Object Store Observables by Property Value](https://developer.fireflysemantics.com/tasks/tasks--slice--filtering-firefly-semantics-slice-object-store-observables-by-property-value) 587 | 588 | 589 | ## Youtube 590 | 591 | - [What is Reactive State Management](https://youtu.be/kEta1LBVw0c) 592 | - [An Introduction to the Firefly Semantics Slice Reactive Object Store](https://youtu.be/_3_mEKw3bM0) 593 | - [Introduction to the Firefly Semantics Slice Reactive Entity Store ](https://youtu.be/Boj3-va-TKk) 594 | - [Creating a Reactive Todo Application With the Firefly Semantics Slice State Manager](https://youtu.be/Y3AxSIiBdWg) 595 | - [Recreating the Ngrx Demo with Slice](https://youtu.be/4t95RvJSY_8) 596 | - [Setting and Observing the Firefly Semantics Slice Entity Store Query](https://youtu.be/_L5ya1CWaYU) 597 | - [Observing the Count of Items in a Firefly Semantics Slice Entity Store](https://youtu.be/5kqr_XW2QuI) 598 | - [Taking a Count Snapshot of a Firefly Semantics Slice Entity Store](https://youtu.be/n37sz4LPV08) 599 | - [Taking a Query Snapshot of a Firefly Semantics Slice Entity Store](https://youtu.be/AFk5p0pNxSk) 600 | - [Adding Slices to an Firefly Semantics Slice Entity Store](https://youtu.be/z2U6OTAsc4I) 601 | - [Adding Search To the Firefly Semantics Slice Angular Todo Application](https://youtu.be/OkiBnU3Q6RU) 602 | - [Converting the Angular Todo Application From Materialize to Angular Material](https://youtu.be/GPfF31hwxQk) 603 | - [Firefly Semantics Slice Entity Store Active API Guide](https://youtu.be/fInpMcZ9Ry8) 604 | - [Comparing Firefly Semantics Slice Entities](https://youtu.be/AYc3Pf9fSKg) 605 | - [Derived Reactive Observable State with Slice](https://youtu.be/eDJkSgYhFIM) 606 | 607 | ## Examples 608 | 609 | - [Minimal Slice Object Store](https://developer.fireflysemantics.com/examples/examples--slice--minimal-slice-object-store) 610 | - [Minimal Angular Slice Object Store State Service](https://developer.fireflysemantics.com/examples/examples--slice--minial-angular-slice-object-store-state-service) 611 | 612 | # API Reference 613 | 614 | The [Typedoc API Reference](https://fireflysemantics.github.io/slice/typedoc/) includes simple examples of how to apply the API for the various stores, methods, and classes included. 615 | 616 | ## Getting Help 617 | 618 | [Firefly Semantics Slice on Stackoverflow](https://stackoverflow.com/questions/tagged/fireflysemantics-slice) 619 | 620 | ## Build 621 | 622 | Run `npm run c` to build the project. The build artifacts will be stored in the `dist/` directory. 623 | 624 | ## Running unit tests 625 | 626 | Run `npm run test` to execute the unit tests. 627 | 628 | ## Tests 629 | 630 | See the [test cases](https://github.com/fireflysemantics/slice/). -------------------------------------------------------------------------------- /VERSIONS.md: -------------------------------------------------------------------------------- 1 | # Supported Versions 2 | 3 | We currently *officially* support Angular 15,16, and 17. 4 | 5 | If you need a build for earlier versions of Angular here is how this can be accomplished. 6 | 7 | # Adding a Branch for a Specific Angular Version 8 | 9 | Here we will describe how we added a library build for version `16.2.1` of Angular. 10 | 11 | Clone slice. 12 | ``` 13 | git clone git@github.com:fireflysemantics/slice.git && cd slice 14 | ``` 15 | 16 | And get all the branches. 17 | ``` 18 | git pull --all 19 | ``` 20 | 21 | Lets first figure out what version of Angular we want. 22 | 23 | ``` 24 | npm view @angular/cli versions --json 25 | ``` 26 | 27 | Then install the version of the CLI that we want. In this case `16.2.x`. 28 | 29 | ``` 30 | npm install -g @angular/cli@16.x.x 31 | ``` 32 | 33 | 34 | Later on we can reinstall the latest version of the CLI like this. 35 | 36 | ``` 37 | npm uninstall -g @angular-cli 38 | npm install -g @angular/cli@latest 39 | ``` 40 | 41 | First create a new version branch. 42 | 43 | ``` 44 | git switch --orphan v16.2.10 45 | ``` 46 | 47 | Create a new Angular version 16 project. 48 | 49 | ``` 50 | ng new --directory . --create-application=false 51 | ``` 52 | 53 | Then generate a slice library. 54 | 55 | ``` 56 | ng g library slice 57 | ``` 58 | 59 | Commit the newly generated library. 60 | 61 | ``` 62 | git add . && git commit -m "Generate slice 16.2.1 workspace project" 63 | ``` 64 | 65 | Remove the `src` folder. 66 | 67 | ``` 68 | git rm -r projects/slice/src 69 | ``` 70 | 71 | Commit thew new structure. 72 | 73 | ``` 74 | git add . && git commit -m "Remove the library source files" 75 | ``` 76 | 77 | Change directories to the `slice` folder. 78 | 79 | 80 | Copy over the slice files from the `master` branch. 81 | 82 | ``` 83 | git checkout master -- projects/slice/src 84 | ``` 85 | 86 | Then update the workspace `package.json` build scripts. 87 | 88 | ``` 89 | "ig": "npm install -g @jsdevtools/version-bump-prompt && npm install -g npm-check-updates && npm install -g npm-install-peers", 90 | "c": "ng build slice", 91 | "bp": "cd projects/slice && bump patch", 92 | "p": "npm run cp && npm run bp && npm run c && cd dist/slice/ && npm publish", 93 | "cp": "cp ./README.md projects/slice/", 94 | "d": "typedoc --out doc --exclude **/*.spec.ts ./projects/slice/src/lib" 95 | ``` 96 | 97 | Commit the update. 98 | 99 | ``` 100 | git add package.json && git commit -m "Update the package.json scripts" 101 | ``` 102 | 103 | Then update `package.json` for the library. 104 | 105 | Change the `name` to `@fireflysemantics/slice`, the `version` to `16.2.10` (The version of the CLI we are using), and change the `peerDependencies` to. 106 | 107 | ``` 108 | "nanoid": "^5.0.4", 109 | "@types/nanoid": "*", 110 | "rxjs": "*" 111 | ``` 112 | 113 | Add the `package.json` `repository`, `bugs`, and `keywords` as blocks as well. 114 | 115 | Commit the update. 116 | 117 | ``` 118 | git add projects/slice/package.json && git commit -m "Update the slice library package.json" 119 | ``` 120 | 121 | Add the root level `README.md`. 122 | 123 | ``` 124 | git add README.md && git commit -m "Update the README.md" 125 | ``` 126 | Publish the version. 127 | ``` 128 | npm run p 129 | 130 | Push the branch. 131 | ``` 132 | git push --set-upstream origin v16.2.10 133 | ``` 134 | 135 | And just commmit the branch after publish. 136 | 137 | ``` 138 | git add . && git commit -m "Post first publish commit" 139 | ``` -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "slice": { 7 | "projectType": "library", 8 | "root": "projects/slice", 9 | "sourceRoot": "projects/slice/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/slice/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/slice/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/slice/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:karma", 29 | "options": { 30 | "tsConfig": "projects/slice/tsconfig.spec.json", 31 | "polyfills": [ 32 | "zone.js", 33 | "zone.js/testing" 34 | ] 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /deploy-doc.js: -------------------------------------------------------------------------------- 1 | var ghpages = require("gh-pages"); 2 | 3 | ghpages.publish("doc", 4 | { 5 | dest: 'typedoc', 6 | repo: "git@github.com:fireflysemantics/slice.git" 7 | }, 8 | function (err) { 9 | if (err) { 10 | console.error(err); 11 | } 12 | } 13 | ); -------------------------------------------------------------------------------- /logo/logo-for-export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 64 | 71 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 62 | 73 | 90 | 101 | 108 | 119 | 126 | 137 | 144 | 155 | 162 | S SLICE LOGO WITH CUTOUT 184 | 185 | 186 | -------------------------------------------------------------------------------- /logo/optimized-svg/logo-for-export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /logo/slicelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireflysemantics/slice/002192fa596d7b0253eda121750717b3feed4500/logo/slicelogo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fireflysemantics/slice", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test --watch=false --browsers=ChromeHeadless", 10 | "i": "npm install -g @jsdevtools/version-bump-prompt && npm install -g npm-check-updates && npm install -g npm-install-peers http-server && npm i -D gh-pages typedoc", 11 | "c": "ng build slice", 12 | "bp": "cd projects/slice && bump patch", 13 | "p": "npm run cp && npm run bp && npm run c && cd dist/slice/ && npm publish", 14 | "cp": "cp ./README.md projects/slice/", 15 | "doc": "rm -fr doc && typedoc --entryPointStrategy expand ./projects/slice/src/lib --out doc --exclude **/*.spec.ts && cp slicelogo.png doc", 16 | "sdoc": "npm run doc && http-server -o doc", 17 | "d": "typedoc --out doc --exclude **/*.spec.ts ./projects/slice/src/lib", 18 | "deploy-doc": "node deploy-doc", 19 | "prepare": "husky && husky install" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@angular/animations": "^17.0.0", 24 | "@angular/common": "^17.0.0", 25 | "@angular/compiler": "^17.0.0", 26 | "@angular/core": "^17.0.0", 27 | "@angular/forms": "^17.0.0", 28 | "@angular/platform-browser": "^17.0.0", 29 | "@angular/platform-browser-dynamic": "^17.0.0", 30 | "@angular/router": "^17.0.0", 31 | "rxjs": "~7.8.0", 32 | "tslib": "^2.3.0", 33 | "zone.js": "~0.14.2" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "^17.0.6", 37 | "@angular/cli": "^17.0.6", 38 | "@angular/compiler-cli": "^17.0.0", 39 | "@commitlint/cli": "^19.2.1", 40 | "@commitlint/config-conventional": "^19.1.0", 41 | "@types/jasmine": "~5.1.0", 42 | "gh-pages": "^6.1.0", 43 | "husky": "^9.0.11", 44 | "jasmine-core": "~5.1.0", 45 | "karma": "~6.4.0", 46 | "karma-chrome-launcher": "~3.2.0", 47 | "karma-coverage": "~2.2.0", 48 | "karma-jasmine": "~5.1.0", 49 | "karma-jasmine-html-reporter": "~2.1.0", 50 | "lint-staged": "^15.2.2", 51 | "ng-packagr": "^17.0.0", 52 | "prettier": "^3.2.5", 53 | "typedoc": "^0.25.4", 54 | "typescript": "~5.2.2" 55 | }, 56 | "lint-staged": { 57 | "*.{js,ts,css,md}": "prettier --write" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /projects/slice/README.md: -------------------------------------------------------------------------------- 1 | ![Slice](slicelogo.png) 2 | 3 | # @fireflysemantics/slice 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [Why Slice](#why-slice) 9 | - [Install](#install) 10 | - [API Reference](#api-reference) 11 | - [Object Store Core Use Cases](#object-store-core-use-cases) 12 | - [Entity Store Core Use Cases](#entity-store-core-use-cases) 13 | - [Features](#features) 14 | - [Help Center Documentation and Media](#firefly-semantics-slice-development-center-media-and-documentation) 15 | - [Getting Help](#getting-help) 16 | 17 | ## Overview 18 | 19 | Lightweight Javascript Reactive State Management for Angular Applications. 20 | 21 | The API is designed to be as minimal as possible and should deliver the same features as other comparable frameworks with about 1/3 the lines of code. 22 | 23 | It offers two types of reactive data stores: 24 | - Entity stores (EStore) for structured entity like data (Customer, Product, User, ...). Entity stores can be "Live filtered" by adding slices. For example separating Todo entities into complete and incomplete compartments. Slices are also obserable. 25 | - Object store (Key value store) for unstructured data 26 | 27 | Even though Angular is used for prototype applications, it should work well in general for: 28 | - Single page applications 29 | - Progressive web applications 30 | - Node applications / Pure Javascript Applications 31 | - Mobile Applications 32 | 33 | If you like the [@fireflysemantics/slice API](https://fireflysemantics.github.io/slice/doc/) please star our [Github Repository](https://github.com/fireflysemantics/slice). 34 | 35 | # Why Slice 36 | 37 | We built Slice to make sharing state between Angular components, services, and other directives simple and in the process we targeted common use cases that should be handled by a state manager, such as updating a shopping cart count, emitting a search query, tracking active state, etc. 38 | 39 | For performing state CRUD operations Slice uses a REST like API, which should be a familiar paradigm for most developers. 40 | 41 | For example a `Todo` entity store tracking todo entities can create, read, update, and delete `Todo` entities as follows (This is just a tiny example of all the capabilities Slice has). 42 | 43 | ``` 44 | let store: EStore = new EStore(); 45 | //============================================ 46 | // Post (Create) a Todo instance in the store 47 | //============================================ 48 | store.post(todo); 49 | //============================================ 50 | // Snapshot of all the Todo entities in the 51 | // store 52 | //============================================ 53 | let snapshot: Todo[] = store.allSnapshot(); 54 | //============================================ 55 | // Observe the array of Todo instances in the 56 | // store 57 | //============================================ 58 | store.obs.subscribe((todos: Todo[]) => { 59 | console.log(`The store is initialized with these Todo entities ${todos}`); 60 | }); 61 | //============================================ 62 | // Delete a Todo instance in the store 63 | //============================================ 64 | todo.complete = false; 65 | store.put(todo); 66 | //============================================ 67 | // Delete a Todo instance in the store 68 | //============================================ 69 | store.delete(todo); 70 | ``` 71 | 72 | # Install 73 | 74 | 75 | Install Slice with the `nanoid` peer dependency: 76 | 77 | - `v17.0.x` for Angular 17 78 | - `v16.2.x` for Angular 16 79 | - `v15.2.x` for Angular 15 80 | 81 | So for example for an Angular 15 project run. 82 | 83 | ``` 84 | npm i @fireflysemantics/slice@15.2.x nanoid 85 | ``` 86 | For Angular 17 run. 87 | 88 | ``` 89 | npm i @fireflysemantics/slice@lastest nanoid 90 | ``` 91 | 92 | # Usage 93 | 94 | The project [typedoc](https://fireflysemantics.github.io/slice/typedoc/), in addition to providing more detailed API insight, syntax highlights all the examples provided here, thus you may want to check it out for a richer reading experience. 95 | 96 | ## Object Store Core Use Cases 97 | 98 | [Here is a link to the Stackblitz Demo](https://stackblitz.com/edit/typescript-9snt67?file=index.ts) 99 | containing all of the below examples. 100 | 101 | In this demo we are using simple `string` values, but we could have used objects or essentially anything that can be referenced by Javascript. 102 | 103 | ``` 104 | import { 105 | KeyObsValueReset, 106 | ObsValueReset, 107 | OStore, 108 | OStoreStart, 109 | } from '@fireflysemantics/slice'; 110 | const START: OStoreStart = { 111 | K1: { value: 'V1', reset: 'ResetValue' }, 112 | }; 113 | interface ISTART extends KeyObsValueReset { 114 | K1: ObsValueReset; 115 | } 116 | let OS: OStore = new OStore(START); 117 | 118 | //============================================ 119 | // Log a snapshot the initial store value. 120 | // This will log 121 | // V1 122 | //============================================ 123 | const v1Snapshot: string = OS.snapshot(OS.S.K1); 124 | console.log(`The value for the K1 key is ${v1Snapshot}`); 125 | 126 | //============================================ 127 | // Observe the initial store value. 128 | // The subsription will log 129 | // V1 130 | //============================================ 131 | OS.S.K1.obs.subscribe((v) => console.log(`The subscribed to value is ${v}`)); 132 | 133 | //============================================ 134 | // Update the initial store value 135 | // The subsription will log 136 | // New Value 137 | //============================================ 138 | OS.put(OS.S.K1, 'New Value'); 139 | 140 | //============================================ 141 | // Log a count of the number of entries in the 142 | // object store. 143 | // This will log 144 | // 1 145 | //============================================ 146 | const count: number = OS.count(); 147 | console.log( 148 | `The count of the number of entries in the Object Store is ${count}` 149 | ); 150 | 151 | //============================================ 152 | // Reset the store 153 | // The subsription will log 154 | // ResetValue 155 | // 156 | // However if we had not specified a reset 157 | // value it would have logged in value 158 | // V1 159 | //============================================ 160 | OS.reset(); 161 | 162 | //============================================ 163 | // Delete the K1 entry 164 | // The subsription will log and the snapshot 165 | // will also be 166 | // undefined 167 | //============================================ 168 | OS.delete(OS.S.K1); 169 | const snapshot: string = OS.snapshot(OS.S.K1); 170 | console.log(`The deleted value snapshot for the K1 key is ${snapshot}`); 171 | 172 | //============================================ 173 | // Clear the store. First we will put a new 174 | // value back in the store to demonstrate it 175 | // being cleared. 176 | //============================================ 177 | //============================================ 178 | // Update the initial store value 179 | // The subsription will log 180 | // New Value 181 | //============================================ 182 | OS.put(OS.S.K1, 'V2'); 183 | 184 | OS.clear(); 185 | //============================================ 186 | // Count the number of values in the store 187 | // It will be zero. 188 | // The OS.clear() call will remove all the 189 | // entries and so the snapshot will be undefined 190 | // and the subscribed to value also undefined. 191 | // The count will be zero. 192 | //============================================ 193 | console.log(`The count is ${OS.count()}`); 194 | console.log(`The snapshot is ${OS.snapshot(OS.S.K1)}`); 195 | ``` 196 | 197 | ## Entity Store Core Use Cases 198 | 199 | [Here is a link to the Stackblitz demo](https://stackblitz.com/edit/typescript-wayluo?file=index.ts) containing the below demo code. You may also wish to check out the [test cases](https://github.com/fireflysemantics/slice/blob/master/projects/slice/src/lib/EStore.spec.ts) for the entity store which also detail usage scenarios. 200 | 201 | ``` 202 | //============================================ 203 | // Demo Utilities 204 | //============================================ 205 | 206 | export const enum TodoSliceEnum { 207 | COMPLETE = 'Complete', 208 | INCOMPLETE = 'Incomplete', 209 | } 210 | 211 | export class Todo { 212 | constructor( 213 | public complete: boolean, 214 | public title: string, 215 | public gid?: string, 216 | public id?: string 217 | ) {} 218 | } 219 | 220 | export const extraTodo: Todo = new Todo(false, 'Do me later.'); 221 | 222 | export let todos = [ 223 | new Todo(false, 'You complete me!'), 224 | new Todo(true, 'You completed me!'), 225 | ]; 226 | 227 | export function todosFactory(): Todo[] { 228 | return [ 229 | new Todo(false, 'You complete me!'), 230 | new Todo(true, 'You completed me!'), 231 | ]; 232 | } 233 | 234 | export function todosClone(): Todo[] { 235 | return todos.map((obj) => ({ ...obj })); 236 | } 237 | 238 | //============================================ 239 | // API: constructor() 240 | // 241 | // Create a Todo Entity Store 242 | //============================================ 243 | let store: EStore = new EStore(todosFactory()); 244 | 245 | //============================================ 246 | // API: post, put, delete 247 | // 248 | // Perform post (Create), put (Update), and delete opeartions 249 | // on the store. 250 | //============================================ 251 | const todoLater: Todo = new Todo(false, 'Do me later.'); 252 | todoLater.id = 'findMe'; 253 | store.post(todoLater); 254 | const postedTodo = store.findOneByID('findMe'); 255 | postedTodo.title = 'Do me sooner'; 256 | store.put(postedTodo); 257 | store.delete(postedTodo); 258 | 259 | //============================================ 260 | // API: allSnapshot() 261 | // 262 | // Take a snapshot of all the entities 263 | // in the store 264 | //============================================ 265 | let snapshot: Todo[] = store.allSnapshot(); 266 | 267 | //============================================ 268 | // API: obs 269 | // 270 | // Create a subscription to the entities in 271 | // the store. 272 | //============================================ 273 | let todosSubscription: Subscription = store.obs.subscribe((todos: Todo[]) => { 274 | console.log(`The store todos ${todos}`); 275 | }); 276 | 277 | //============================================ 278 | // API: findOne() 279 | // 280 | // Find a Todo instance using the 281 | // Global ID (guid) property. 282 | //============================================ 283 | const globalID: string = '1'; 284 | let findThisTodo = new Todo(false, 'Find this Todo', globalID); 285 | 286 | store.post(findThisTodo); 287 | 288 | const todo = store.findOne(globalID); 289 | console.log(todo); 290 | 291 | //============================================ 292 | // API: findOneByID() 293 | // 294 | // Find a Todo instance using the 295 | // ID (id) property. 296 | //============================================ 297 | const ID: string = 'id'; 298 | let todoWithID = new Todo(false, 'Find this Todo by ID'); 299 | todoWithID.id = ID; 300 | 301 | store.post(todoWithID); 302 | const todoFoundByID = store.findOneByID(ID); 303 | 304 | console.log(`The Todo instance found by id is ${todoFoundByID}`); 305 | 306 | //============================================ 307 | // API: select() 308 | // 309 | // Select Todo instances where the title 310 | // includes the string Find. 311 | //============================================ 312 | const selectLaterPredicate: Predicate = (todo: Todo) => { 313 | return todo.title.includes('Find'); 314 | }; 315 | const selections = store.select(selectLaterPredicate); 316 | console.log( 317 | `The selected todo instances that contain Find are: ${selections.length}` 318 | ); 319 | 320 | //============================================ 321 | // API: observeLoading() 322 | // 323 | // Subscribe to the store loading indicator 324 | // and toggle it to see the values change. 325 | //============================================ 326 | store.observeLoading().subscribe((loading) => { 327 | console.log(`Is data loading: ${loading}`); 328 | }); 329 | store.loading = true; 330 | store.loading = false; 331 | 332 | //============================================ 333 | // API: observeSearching() 334 | // 335 | // Subscribe to the store searching indicator 336 | // and toggle it to see the values change. 337 | //============================================ 338 | store.observeSearching().subscribe((searching) => { 339 | console.log(`Is the store searching: ${searching}`); 340 | }); 341 | store.searching = true; 342 | store.searching = false; 343 | 344 | //============================================ 345 | // API: addActive() 346 | // Perform active state tracking. Initially the 347 | // number of active entities will be zero. 348 | //============================================ 349 | console.log(`The number of active Todo instances is ${store.active.size}`); 350 | let todo1: Todo = new Todo(false, 'The first Todo!', GUID()); 351 | let todo2: Todo = new Todo(false, 'The first Todo!', GUID()); 352 | store.addActive(todo1); 353 | console.log(`The number of active Todo instances is ${store.active.size}`); 354 | 355 | console.log( 356 | `The number of active Todo instances by the activeSnapshot is ${ 357 | store.activeSnapshot().length 358 | }` 359 | ); 360 | 361 | //============================================ 362 | // API: observeActive() 363 | // 364 | // Subscribing to the observeActive() observable 365 | // provides the map of active Todo instances. 366 | //============================================ 367 | store.observeActive().subscribe((active) => { 368 | console.log(`The active Todo instances are: ${active}`); 369 | }); 370 | 371 | //============================================ 372 | // API: deleteActive() 373 | // Delete the active Todo instance. 374 | // This will set the number of active 375 | // Todo instances back to zero. 376 | //============================================ 377 | store.deleteActive(todo1); 378 | console.log( 379 | `The number of active Todo instances by the activeSnapshot is ${ 380 | store.activeSnapshot().length 381 | }` 382 | ); 383 | 384 | //============================================ 385 | // API: count() and snapshotCount() 386 | // 387 | // Take snapshot and observable 388 | // the counts of store entities 389 | //============================================ 390 | 391 | const completePredicate: Predicate = function pred(t: Todo) { 392 | return t.complete; 393 | }; 394 | 395 | const incompletePredicate: Predicate = function pred(t: Todo) { 396 | return !t.complete; 397 | }; 398 | 399 | store.count().subscribe((c) => { 400 | console.log(`The observed count of Todo entities is ${c}`); 401 | }); 402 | store.count(incompletePredicate).subscribe((c) => { 403 | console.log(`The observed count of incomplete Todo enttiies is ${c}`); 404 | }); 405 | store.count(completePredicate).subscribe((c) => { 406 | console.log(`The observed count of complete Todo enttiies is ${c}`); 407 | }); 408 | 409 | const snapshotCount = store.countSnapshot(completePredicate); 410 | console.log(`The count is ${snapshotCount}`); 411 | 412 | const completeSnapshotCount = store.countSnapshot(completePredicate); 413 | console.log( 414 | `The complete Todo Entity Snapshot count is ${completeSnapshotCount}` 415 | ); 416 | 417 | const incompleteSnapshotCount = store.countSnapshot(incompletePredicate); 418 | console.log( 419 | `The incomplete Todo Entity Snapshot count is ${incompleteSnapshotCount}` 420 | ); 421 | 422 | 423 | //============================================ 424 | // API: toggle() 425 | // 426 | // When we post another todo using toggle 427 | // instance the subscribed to count 428 | // dynamically increases by 1. 429 | // When we call toggle again, 430 | // removing the instance the 431 | // count decreases by 1. 432 | //============================================ 433 | store.toggle(extraTodo); 434 | store.toggle(extraTodo); 435 | 436 | //============================================ 437 | // API: contains() 438 | // 439 | // When we post another todo using toggle 440 | // the store now contains it. 441 | //============================================ 442 | console.log( 443 | `Does the store contain the extraTodo ${store.contains(extraTodo)}` 444 | ); 445 | store.toggle(extraTodo); 446 | console.log( 447 | `Does the store contain the extraTodo ${store.contains(extraTodo)}` 448 | ); 449 | store.toggle(extraTodo); 450 | console.log( 451 | `Does the store contain the extraTodo ${store.contains(extraTodo)}` 452 | ); 453 | 454 | //============================================ 455 | // API: containsbyID() 456 | // 457 | // When we post another todo using toggle 458 | // the store now contains it. 459 | // 460 | // Note the containsByID() can be called with 461 | // both the id property or the entire instance. 462 | //============================================ 463 | let todoByID = new Todo(false, 'This is not in the store', undefined, '1'); 464 | store.post(todoByID); 465 | console.log( 466 | `Does the store contain the todoByID ${store.containsById(todoByID.id)}` 467 | ); 468 | console.log( 469 | `Does the store contain the todoByID ${store.containsById(todoByID)}` 470 | ); 471 | store.toggle(todoByID); 472 | console.log( 473 | `Does the store contain the todoByID ${store.containsById(todoByID.id)}` 474 | ); 475 | console.log( 476 | `Does the store contain the todoByID ${store.containsById(todoByID)}` 477 | ); 478 | 479 | //============================================ 480 | // API: equalsByGUID and equalsByID 481 | // 482 | // Compare entities by ID and Global ID (guid). 483 | // We will assign the ID and the global ID 484 | // instead of allowing the global ID to be 485 | // assigned by the store on post. 486 | //============================================ 487 | const guid = GUID(); 488 | let todoOrNotTodo1 = new Todo(false, 'Apples to Apples', guid, '1'); 489 | let todoOrNotTodo2 = new Todo(false, 'Apples to Apples', guid, '1'); 490 | 491 | const equalByID: boolean = store.equalsByID(todoOrNotTodo1, todoOrNotTodo2); 492 | console.log(`Are the todos equal by id: ${equalByID}`); 493 | const equalByGUID: boolean = store.equalsByGUID(todoOrNotTodo1, todoOrNotTodo2); 494 | console.log(`Are the todos equal by global id: ${equalByGUID}`); 495 | 496 | //============================================ 497 | // API: addSlice 498 | // 499 | // Add a slice for complete todo entities. 500 | // 501 | // We create a new store to demo with a 502 | // consistent count. 503 | // 504 | // When posting the extraTodo which is 505 | // incomplete, we see that the incomplete 506 | // count increments. 507 | //============================================ 508 | store.destroy(); 509 | store = new EStore(todosFactory()); 510 | store.addSlice((todo) => todo.complete, TodoSliceEnum.COMPLETE); 511 | store.addSlice((todo) => !todo.complete, TodoSliceEnum.INCOMPLETE); 512 | const completeSlice = store.getSlice(TodoSliceEnum.COMPLETE); 513 | const incompleteSlice = store.getSlice(TodoSliceEnum.INCOMPLETE); 514 | completeSlice.count().subscribe((c) => { 515 | console.log(`The number of entries in the complete slice is ${c}`); 516 | }); 517 | incompleteSlice.count().subscribe((c) => { 518 | console.log(`The number of entries in the incomplete slice is ${c}`); 519 | }); 520 | store.post(extraTodo); 521 | const incompleteTodos: Todo[] = incompleteSlice.allSnapshot(); 522 | console.log(`The incomplete Todo entities are ${incompleteTodos}`); 523 | 524 | //============================================ 525 | // API: isEmpty() 526 | // 527 | // Check whether the store is empty. 528 | //============================================ 529 | store.isEmpty().subscribe((empty) => { 530 | console.log(`Is the store empty? ${empty}`); 531 | }); 532 | ``` 533 | 534 | ## Features 535 | 536 | - Live Stackblitz demoes 537 | - [Typedoc with inlined examples](https://fireflysemantics.github.io/slice/typedoc/) 538 | - [Well documented test cases run with Jest - Each file has a corresponding `.spec` file](https://github.com/fireflysemantics/slice/tree/master/src) 539 | - Stream both Entity and Object Stores for UI Updates via RxJS 540 | - Define entities using Typescript classes, interfaces, or types 541 | - [Active state tracking](https://medium.com/@ole.ersoy/monitoring-the-currently-active-entity-with-slice-ff7c9b7826e8) 542 | - [Supports for Optimistic User Interfaces](https://medium.com/@ole.ersoy/optimistic-user-identity-management-with-slice-a2b66efe780c) 543 | - RESTful API for performing CRUD operations that stream both full and delta updates 544 | - Dynamic creation of both object and entity stores 545 | - Observable delta updates for Entities 546 | - Real time application of Slice `Predicate` filtering that is `Observable` 547 | - `Predicate` based snapshots of entities 548 | - Observable `count` of entities in the entity store. The `count` feature can also be `Predicate` filtered. 549 | - Configurable global id (Client side id - `gid`) and server id (`id`) id property names for entities. 550 | - The stream of entities can be sorted via an optional boolean expression passed to `observe`. 551 | 552 | # Firefly Semantics Slice Development Center Media and Documentation 553 | 554 | ## Concepts 555 | 556 | - [What is Reactive State Management](https://developer.fireflysemantics.com/concepts/concepts--slice--what-is-reactive-state-management) 557 | 558 | ## Guides 559 | 560 | - [An Introduction to the Firefly Semantics Slice Reactive Object Store](https://developer.fireflysemantics.com/guides/guides--introduction-to-the-firefly-semantics-slice-reactive-object-store) 561 | - [Introduction to the Firefly Semantics Slice Reactive Entity Store ](https://developer.fireflysemantics.com/guides/guides--introduction-to-the-firefly-semantics-slice-reactive-entity-store) 562 | - [Creating a Reactive Todo Application With the Firefly Semantics Slice State Manager](https://developer.fireflysemantics.com/guides/guides--slice--creating-a-reactive-todo-application-with-the-firefly-semantics-slice-state-manager) 563 | - [Recreating the Ngrx Demo with Slice](https://developer.fireflysemantics.com/guides/guides--recreating-the-ngrx-demo-app-with-firefly-semantics-slice-state-manager) 564 | - [Firefly Semantics Slice Entity Store Active API Guide](https://developer.fireflysemantics.com/guides/guides--slice--managing-active-entities-with-firefly-semantics-slice) 565 | 566 | 567 | ## Tasks 568 | 569 | - [Creating a Minimal Slice Object Store](https://developer.fireflysemantics.com/examples/examples--slice--minimal-slice-object-store) 570 | - [Creating a Minimal Angular Slice Object Store Angular State Service ](https://developer.fireflysemantics.com/examples/examples--slice--minial-angular-slice-object-store-state-service) 571 | - [Changing the Firefly Semantics Slice EStore Default Configuration](https://developer.fireflysemantics.com/tasks/tasks--slice--changing-the-fireflysemantics-slice-estore-default-configuration) 572 | - [Observing the Currently Active Entities with Slice](https://developer.fireflysemantics.com/tasks/tasks--slice--observing-currently-active-entities-with-slice) 573 | - [Derived Reactive Observable State with Slice](https://developer.fireflysemantics.com/tasks/tasks--slice--derived-reactive-observable-state-with-slice) 574 | - [Reactive Event Driven Actions with Firefly Semantics Slice](https://developer.fireflysemantics.com/tasks/tasks--slice--reactive-event-driven-actions-with-firefly-semantics-slice) 575 | - [Unsubscribing From Firefly Semantics Slice Object Store Observables in Angular](https://developer.fireflysemantics.com/tasks/tasks--slice--unsubscribing-from-firefly-semantics-slice-object-store-observables-in-angular) 576 | - [Creating Proxies to Slice Object Store Observables](https://developer.fireflysemantics.com/tasks/tasks--slice--creating-proxies-to-slice-object-store-observables) 577 | - [Getting a Snapshot of a Slice Object Store Value](https://developer.fireflysemantics.com/tasks/tasks--slice--getting-a-snapshot-of-a-slice-object-store-value) 578 | - [Accessing Slice Object Store Observables In Angular Templates](https://developer.fireflysemantics.com/tasks/tasks--slice--accessing-slice-object-store-observables-in-angular-templates) 579 | - [Observing the Count of Items in a Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--observing-the-count-of-items-in-a-firefly-semantics-slice-entity-store) 580 | - [Setting and Observing Firefly Semantics Slice Entity Store Queries](https://developer.fireflysemantics.com/tasks/tasks--slice--setting-and-observing-firefly-semantics-slice-entity-store-queries) 581 | - [Taking a Count Snapshot of a Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--taking-a-count-snapshot-of-a-firefly-semantics-slice-entity-store) 582 | - [Taking a Query Snapshot of a Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--taking-a-query-snapshot-of-a-firefly-semantics-slice-entity-store) 583 | - [Adding Slices to an Firefly Semantics Slice Entity Store](https://developer.fireflysemantics.com/tasks/tasks--slice--adding-slices-to-the-firefly-semantics-entity-store) 584 | - [Adding Search To the Firefly Semantics Slice Angular Todo Application](https://developer.fireflysemantics.com/tasks/tasks--slice--adding-search-to-the-firefly-semantics-slice-angular-todo-application) 585 | - [Comparing Firefly Semantics Slice Entities](https://developer.fireflysemantics.com/tasks/tasks--slice--comparing-firefly-semantics-slice-entities) 586 | - [Filtering Firefly Semantics Slice Object Store Observables by Property Value](https://developer.fireflysemantics.com/tasks/tasks--slice--filtering-firefly-semantics-slice-object-store-observables-by-property-value) 587 | 588 | 589 | ## Youtube 590 | 591 | - [What is Reactive State Management](https://youtu.be/kEta1LBVw0c) 592 | - [An Introduction to the Firefly Semantics Slice Reactive Object Store](https://youtu.be/_3_mEKw3bM0) 593 | - [Introduction to the Firefly Semantics Slice Reactive Entity Store ](https://youtu.be/Boj3-va-TKk) 594 | - [Creating a Reactive Todo Application With the Firefly Semantics Slice State Manager](https://youtu.be/Y3AxSIiBdWg) 595 | - [Recreating the Ngrx Demo with Slice](https://youtu.be/4t95RvJSY_8) 596 | - [Setting and Observing the Firefly Semantics Slice Entity Store Query](https://youtu.be/_L5ya1CWaYU) 597 | - [Observing the Count of Items in a Firefly Semantics Slice Entity Store](https://youtu.be/5kqr_XW2QuI) 598 | - [Taking a Count Snapshot of a Firefly Semantics Slice Entity Store](https://youtu.be/n37sz4LPV08) 599 | - [Taking a Query Snapshot of a Firefly Semantics Slice Entity Store](https://youtu.be/AFk5p0pNxSk) 600 | - [Adding Slices to an Firefly Semantics Slice Entity Store](https://youtu.be/z2U6OTAsc4I) 601 | - [Adding Search To the Firefly Semantics Slice Angular Todo Application](https://youtu.be/OkiBnU3Q6RU) 602 | - [Converting the Angular Todo Application From Materialize to Angular Material](https://youtu.be/GPfF31hwxQk) 603 | - [Firefly Semantics Slice Entity Store Active API Guide](https://youtu.be/fInpMcZ9Ry8) 604 | - [Comparing Firefly Semantics Slice Entities](https://youtu.be/AYc3Pf9fSKg) 605 | - [Derived Reactive Observable State with Slice](https://youtu.be/eDJkSgYhFIM) 606 | 607 | ## Examples 608 | 609 | - [Minimal Slice Object Store](https://developer.fireflysemantics.com/examples/examples--slice--minimal-slice-object-store) 610 | - [Minimal Angular Slice Object Store State Service](https://developer.fireflysemantics.com/examples/examples--slice--minial-angular-slice-object-store-state-service) 611 | 612 | # API Reference 613 | 614 | The [Typedoc API Reference](https://fireflysemantics.github.io/slice/typedoc/) includes simple examples of how to apply the API for the various stores, methods, and classes included. 615 | 616 | ## Getting Help 617 | 618 | [Firefly Semantics Slice on Stackoverflow](https://stackoverflow.com/questions/tagged/fireflysemantics-slice) 619 | 620 | ## Build 621 | 622 | Run `npm run c` to build the project. The build artifacts will be stored in the `dist/` directory. 623 | 624 | ## Running unit tests 625 | 626 | Run `npm run test` to execute the unit tests. 627 | 628 | ## Tests 629 | 630 | See the [test cases](https://github.com/fireflysemantics/slice/). -------------------------------------------------------------------------------- /projects/slice/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/slice", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/slice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fireflysemantics/slice", 3 | "version": "17.0.21", 4 | "peerDependencies": { 5 | "nanoid": "^5.0.4", 6 | "@types/nanoid": "*", 7 | "rxjs": "*" 8 | }, 9 | "author": "Ole Ersoy", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/fireflysemantics/slice/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/fireflysemantics/slice.git" 17 | }, 18 | "keywords": [ 19 | "Light Weight", 20 | "Javascript", 21 | "Typescript", 22 | "Angular", 23 | "State", 24 | "Query", 25 | "Reactive", 26 | "RxJS", 27 | "Observables", 28 | "Multicasting" 29 | ], 30 | "dependencies": { 31 | "tslib": "^2.3.0" 32 | }, 33 | "sideEffects": false 34 | } 35 | -------------------------------------------------------------------------------- /projects/slice/src/lib/AbstractStore.ts: -------------------------------------------------------------------------------- 1 | import { ReplaySubject, Observable } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | import { Delta, Predicate } from './models'; 4 | import { StoreConfig } from './models/StoreConfig'; 5 | 6 | const { freeze } = Object; 7 | 8 | const ESTORE_DEFAULT_ID_KEY = 'id'; 9 | const ESTORE_DEFAULT_GID_KEY = 'gid'; 10 | 11 | export const ESTORE_CONFIG_DEFAULT: StoreConfig = freeze({ 12 | idKey: ESTORE_DEFAULT_ID_KEY, 13 | guidKey: ESTORE_DEFAULT_GID_KEY, 14 | }); 15 | 16 | export abstract class AbstractStore { 17 | /** 18 | * The configuration for the store. 19 | */ 20 | public config: StoreConfig; 21 | 22 | constructor(config?: StoreConfig) { 23 | this.config = config 24 | ? freeze({ ...ESTORE_CONFIG_DEFAULT, ...config }) 25 | : ESTORE_CONFIG_DEFAULT; 26 | } 27 | 28 | /** 29 | * Notifies observers of the store query. 30 | */ 31 | protected notifyQuery = new ReplaySubject(1); 32 | 33 | /** 34 | * The current query state. 35 | */ 36 | protected _query: string = ''; 37 | 38 | /** 39 | * Sets the current query state and notifies observers. 40 | */ 41 | set query(query: string) { 42 | this._query = query; 43 | this.notifyQuery.next(this._query); 44 | } 45 | 46 | /** 47 | * @return A snapshot of the query state. 48 | */ 49 | get query() { 50 | return this._query; 51 | } 52 | 53 | /** 54 | * Observe the query. 55 | * @example 56 |
 57 |     let query$ = source.observeQuery();
 58 |     
59 | */ 60 | public observeQuery() { 61 | return this.notifyQuery.asObservable(); 62 | } 63 | 64 | /** 65 | * The current id key for the EStore instance. 66 | * @return this.config.idKey; 67 | */ 68 | get ID_KEY(): string { 69 | return this.config.idKey; 70 | } 71 | /** 72 | * The current guid key for the EStore instance. 73 | * @return this.config.guidKey; 74 | */ 75 | get GUID_KEY(): string { 76 | return this.config.guidKey; 77 | } 78 | 79 | /** 80 | * Primary index for the stores elements. 81 | */ 82 | public entries: Map = new Map(); 83 | 84 | /** 85 | * The element entries that are keyed by 86 | * an id generated on the server. 87 | */ 88 | public idEntries: Map = new Map(); 89 | 90 | /** 91 | * Create notifications that broacast 92 | * the entire set of entries. 93 | */ 94 | protected notify = new ReplaySubject(1); 95 | 96 | /** 97 | * Create notifications that broacast 98 | * store or slice delta state changes. 99 | */ 100 | protected notifyDelta = new ReplaySubject>(1); 101 | 102 | /** 103 | * Call all the notifiers at once. 104 | * 105 | * @param v 106 | * @param delta 107 | */ 108 | protected notifyAll(v: E[], delta: Delta) { 109 | this.notify.next(v); 110 | this.notifyDelta.next(delta); 111 | } 112 | 113 | /** 114 | * Observe store state changes. 115 | * 116 | * @param sort Optional sorting function yielding a sorted observable. 117 | * @example 118 | * ``` 119 | * let todos$ = source.observe(); 120 | * //or with a sort by title function 121 | * let todos$ = source.observe((a, b)=>(a.title > b.title ? -1 : 1)); 122 | * ``` 123 | */ 124 | public observe(sort?: (a: any, b: any) => number): Observable { 125 | if (sort) { 126 | return this.notify.pipe(map((e: E[]) => e.sort(sort))); 127 | } 128 | return this.notify.asObservable(); 129 | } 130 | 131 | /** 132 | * An Observable reference 133 | * to the entities in the store or 134 | * Slice instance. 135 | */ 136 | public obs: Observable = this.observe(); 137 | 138 | /** 139 | * Observe delta updates. 140 | * 141 | * @example 142 | * ``` 143 | * let todos$ = source.observeDelta(); 144 | * ``` 145 | */ 146 | public observeDelta(): Observable> { 147 | return this.notifyDelta.asObservable(); 148 | } 149 | 150 | /** 151 | * Check whether the store is empty. 152 | * 153 | * @return A hot {@link Observable} that indicates whether the store is empty. 154 | * 155 | * @example 156 | * ``` 157 | * const empty$:Observable = source.isEmpty(); 158 | * ``` 159 | */ 160 | isEmpty(): Observable { 161 | return this.notify.pipe(map((entries: E[]) => entries.length == 0)); 162 | } 163 | 164 | /** 165 | * Check whether the store is empty. 166 | * 167 | * @return A snapshot that indicates whether the store is empty. 168 | * 169 | * @example 170 | * ``` 171 | * const empty:boolean = source.isEmptySnapshot(); 172 | * ``` 173 | */ 174 | isEmptySnapshot(): boolean { 175 | return Array.from(this.entries.values()).length == 0; 176 | } 177 | 178 | /** 179 | * Returns the number of entries contained. 180 | * @param p The predicate to apply in order to filter the count 181 | * 182 | * @example 183 | * ``` 184 | * const completePredicate: Predicate = function pred(t: Todo) { 185 | * return t.complete; 186 | * }; 187 | * 188 | * const incompletePredicate: Predicate = function pred(t: Todo) { 189 | * return !t.complete; 190 | * }; 191 | * 192 | * store.count().subscribe((c) => { 193 | * console.log(`The observed count of Todo entities is ${c}`); 194 | * }); 195 | * store.count(incompletePredicate).subscribe((c) => { 196 | * console.log(`The observed count of incomplete Todo enttiies is ${c}`); 197 | * }); 198 | * store.count(completePredicate).subscribe((c) => { 199 | * console.log(`The observed count of complete Todo enttiies is ${c}`); 200 | * }); 201 | * ``` 202 | */ 203 | count(p?: Predicate): Observable { 204 | if (p) { 205 | return this.notify.pipe( 206 | map((e: E[]) => e.reduce((total, e) => total + (p(e) ? 1 : 0), 0)) 207 | ); 208 | } 209 | return this.notify.pipe(map((entries: E[]) => entries.length)); 210 | } 211 | 212 | /** 213 | * Returns a snapshot of the number of entries contained in the store. 214 | * @param p The predicate to apply in order to filter the count 215 | * 216 | * @example 217 | * ``` 218 | * const completePredicate: Predicate = function pred(t: Todo) { 219 | * return t.complete; 220 | * }; 221 | * 222 | * const incompletePredicate: Predicate = function pred(t: Todo) { 223 | * return !t.complete; 224 | * }; 225 | * 226 | * const snapshotCount = store.countSnapshot(completePredicate); 227 | * console.log(`The count is ${snapshotCount}`); 228 | * 229 | * const completeSnapshotCount = store.countSnapshot(completePredicate); 230 | * console.log( 231 | * `The complete Todo Entity Snapshot count is ${completeSnapshotCount}` 232 | * ); 233 | * 234 | * const incompleteSnapshotCount = store.countSnapshot(incompletePredicate); 235 | * console.log( 236 | * `The incomplete Todo Entity Snapshot count is ${incompleteSnapshotCount}` 237 | * ); 238 | * ``` 239 | */ 240 | countSnapshot(p?: Predicate): number { 241 | if (p) { 242 | return Array.from(this.entries.values()).filter(p).length; 243 | } 244 | return Array.from(this.entries.values()).length; 245 | } 246 | 247 | /** 248 | * Snapshot of all entries. 249 | * 250 | * @return Snapshot array of all the elements the entities the store contains. 251 | * 252 | * @example Observe a snapshot of all the entities in the store. 253 | * 254 | * ``` 255 | * let selectedTodos:Todo[] = source.allSnapshot(); 256 | * ``` 257 | */ 258 | allSnapshot(): E[] { 259 | return Array.from(this.entries.values()); 260 | } 261 | 262 | /** 263 | * Returns true if the entries contain the identified instance. 264 | * 265 | * @param target Either an instance of type `E` or a `guid` identifying the instance. 266 | * @param byId Whether the lookup should be performed with the `id` key rather than the `guid`. 267 | * @returns true if the instance identified by the guid exists, false otherwise. 268 | * 269 | * @example 270 | * ``` 271 | * let contains:boolean = source.contains(guid); 272 | * ``` 273 | */ 274 | contains(target: E | string): boolean { 275 | if (typeof target === 'string') { 276 | return this.entries.get(target) ? true : false; 277 | } 278 | const guid: string = (target)[this.config.guidKey]; 279 | return this.entries.get(guid) ? true : false; 280 | } 281 | 282 | /** 283 | * Returns true if the entries contain the identified instance. 284 | * 285 | * @param target Either an instance of type `E` or a `id` identifying the instance. 286 | * @returns true if the instance identified by the `id` exists, false otherwise. 287 | * 288 | * @example 289 | * ``` 290 | * let contains:boolean = source.contains(guid); 291 | * ``` 292 | */ 293 | containsById(target: E | string): boolean { 294 | if (typeof target === 'string') { 295 | return this.idEntries.get(target) ? true : false; 296 | } 297 | const id: string = (target)[this.config.idKey]; 298 | return this.idEntries.get(id) ? true : false; 299 | } 300 | 301 | /** 302 | * Find and return the entity identified by the GUID parameter 303 | * if it exists and return it. 304 | * 305 | * @param guid 306 | * @return The entity instance if it exists, null otherwise 307 | * 308 | * @example 309 | * ``` 310 | * const globalID: string = '1'; 311 | * let findThisTodo = new Todo(false, 'Find this Todo', globalID); 312 | * store.post(findThisTodo); 313 | * const todo = store.findOne(globalID); 314 | * ``` 315 | */ 316 | findOne(guid: string): E | undefined { 317 | return this.entries.get(guid); 318 | } 319 | 320 | /** 321 | * Find and return the entity identified by the ID parameter 322 | * if it exists and return it. 323 | * 324 | * @param id 325 | * @return The entity instance if it exists, null otherwise 326 | * 327 | * @example 328 | * ``` 329 | * const todoLater: Todo = new Todo(false, 'Do me later.'); 330 | * todoLater.id = 'findMe'; 331 | * store.post(todoLater); 332 | * const postedTodo = store.findOneByID('findMe'); 333 | * ``` 334 | */ 335 | findOneByID(id: string): E | undefined { 336 | return this.idEntries.get(id); 337 | } 338 | 339 | /** 340 | * Snapshot of the entries that match the predicate. 341 | * 342 | * @param p The predicate used to query for the selection. 343 | * @return A snapshot array containing the entities that match the predicate. 344 | * 345 | * @example Select all the Todo instances where the title length is greater than 100. 346 | * ``` 347 | * let todos:Todo[]=store.select(todo=>todo.title.length>100); 348 | * ``` 349 | */ 350 | select(p: Predicate): E[] { 351 | const selected: E[] = []; 352 | Array.from(this.entries.values()).forEach((e) => { 353 | if (p(e)) { 354 | selected.push(e); 355 | } 356 | }); 357 | return selected; 358 | } 359 | 360 | /** 361 | * Compare entities by GUID 362 | * 363 | * @param e1 The first entity 364 | * @param e2 The second entity 365 | * @return true if the two entities have equal GUID ids 366 | * 367 | * @example Compare todo1 with todo2 by gid. 368 | * ``` 369 | * if (equalsByGUID(todo1, todo2)){...}; 370 | * ``` 371 | */ 372 | equalsByGUID(e1: any, e2: any) { 373 | return e1[this.GUID_KEY] == e2[this.GUID_KEY]; 374 | } 375 | 376 | /** 377 | * Compare entities by ID 378 | * 379 | * @param e1 The first entity 380 | * @param e2 The second entity 381 | * @return true if the two entities have equal ID ids 382 | * 383 | * @example Compare todo1 with todo2 by id. 384 | * 385 | * ``` 386 | * if (equalsByID(todo1, todo2)){...}; 387 | * ``` 388 | */ 389 | equalsByID(e1: any, e2: any) { 390 | return e1[this.ID_KEY] == e2[this.ID_KEY]; 391 | } 392 | 393 | /** 394 | * Call destroy when disposing of the store, as it 395 | * completes all {@link ReplaySubject} instances. 396 | * 397 | * @example 398 | * ``` 399 | * store.destroy(); 400 | * ``` 401 | */ 402 | destroy() { 403 | this.notify.complete(); 404 | this.notifyDelta.complete(); 405 | this.notifyQuery.complete(); 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /projects/slice/src/lib/EStore.spec.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoSliceEnum, todosFactory } from "./test-setup" 2 | import { GUID, search } from './utilities' 3 | import { EStore } from "./EStore" 4 | import { Slice } from "./Slice" 5 | import { Observable, combineLatest } from "rxjs" 6 | import { of } from 'rxjs' 7 | import { TestScheduler } from 'rxjs/testing'; 8 | import { throttleTime } from 'rxjs/operators'; 9 | 10 | const { values } = Object; 11 | 12 | /** 13 | * CONCERN: Store Initialization 14 | * METHODs: `constructor` 15 | */ 16 | it("should constructor initialize the store", () => { 17 | let store: EStore = new EStore(todosFactory()); 18 | expect(store.entries.size).toEqual(2); 19 | }); 20 | 21 | it('should show that the observable reference works', done => { 22 | let store: EStore = new EStore(todosFactory()); 23 | expect(store.entries.size).toEqual(2); 24 | store.obs.subscribe(todos => { 25 | expect(todos.length).toEqual(2); 26 | done(); 27 | }) 28 | }) 29 | 30 | /** 31 | * CONCERN: Utility API 32 | * METHODS: `findOne`. 33 | */ 34 | it("should findOne", () => { 35 | let todoOrNotTodo = new Todo(false, "This is not in the store", '1'); 36 | let store: EStore = new EStore(); 37 | store.post(todoOrNotTodo); 38 | expect(store.findOne('1')!.complete).toBeFalsy(); 39 | expect(store.findOne('1')!.gid).toEqual('1'); 40 | }); 41 | 42 | /** 43 | * CONCERN: Utility API 44 | * METHODS: `findOneByID`. 45 | */ 46 | it("should findByID", () => { 47 | let todoOrNotTodo = new Todo(false, "This is not in the store", '1'); 48 | let todoOrNot = new Todo(false, "This is not in the store", '2'); 49 | todoOrNotTodo.id = '1'; 50 | todoOrNot.id = '2'; 51 | let store: EStore = new EStore(); 52 | store.post(todoOrNotTodo); 53 | expect(store.findOneByID('1')!.complete).toBeFalsy(); 54 | expect(store.findOneByID('1')!.id).toEqual('1'); 55 | store = new EStore(); 56 | store.postA([todoOrNotTodo, todoOrNot]); 57 | expect(store.findOneByID('1')!.complete).toBeFalsy(); 58 | expect(store.findOneByID('1')!.id).toEqual('1'); 59 | expect(store.findOneByID('2')!.id).toEqual('2'); 60 | store = new EStore(); 61 | store.postN(todoOrNotTodo, todoOrNot); 62 | expect(store.findOneByID('1')!.complete).toBeFalsy(); 63 | expect(store.findOneByID('1')!.id).toEqual('1'); 64 | expect(store.findOneByID('2')!.id).toEqual('2'); 65 | }); 66 | 67 | 68 | /** 69 | * CONCERN: Utility API 70 | * PROPERTY: `loading` 71 | */ 72 | it("should toggle the loading indicator and make it observable", (done) => { 73 | let store: EStore = new EStore(); 74 | store.loading = true; 75 | expect(store.loading).toBeTruthy(); 76 | store.loading = false; 77 | expect(store.loading).toBeFalsy(); 78 | store.loading = true; 79 | let l: Observable = store.observeLoading(); 80 | l.subscribe(loading => { 81 | expect(loading).toEqual(true); 82 | done() 83 | }); 84 | }); 85 | 86 | 87 | /** 88 | * CONCERN: Utility API 89 | * PROPERTY: `searching` 90 | */ 91 | it("should toggle the searching indicator and make it observable", (done) => { 92 | let store: EStore = new EStore(); 93 | store.searching = true; 94 | expect(store.searching).toBeTruthy(); 95 | store.searching = false; 96 | expect(store.searching).toBeFalsy(); 97 | store.searching = true; 98 | let l: Observable = store.observeSearching(); 99 | l.subscribe(searching => { 100 | expect(searching).toEqual(true); 101 | done() 102 | }); 103 | }); 104 | 105 | /** 106 | * CONCERN: Active State 107 | * METHODS: `addActive` and `deleteActive`. 108 | */ 109 | it("should add and delete active state", () => { 110 | let store: EStore = new EStore(); 111 | expect(store.active.size).toEqual(0); 112 | let todo1: Todo = new Todo(false, "The first Todo!", GUID()); 113 | let todo2: Todo = new Todo(false, "The first Todo!", GUID()); 114 | //Will add the entity if it's not contained in the store. 115 | store.addActive(todo1); 116 | expect(store.active.size).toEqual(1); 117 | expect(store.activeSnapshot().length).toEqual(1) 118 | store.post(todo1); 119 | store.post(todo2); 120 | store.addActive(todo1); 121 | expect(store.active.size).toEqual(1); 122 | store.addActive(todo2); 123 | let a: Observable> = store.observeActive(); 124 | let s = a.subscribe(active => { 125 | expect(active.get(todo1.gid!)).toEqual(todo1); 126 | expect(active.get(todo2.gid!)).toEqual(todo2); 127 | }); 128 | s.unsubscribe(); 129 | store.deleteActive(todo1); 130 | expect(store.active.size).toEqual(1); 131 | store.deleteActive(todo2); 132 | expect(store.active.size).toEqual(0); 133 | expect(store.entries.size).toEqual(2); 134 | }); 135 | 136 | 137 | /** 138 | * CONCERN: Utility API 139 | * METHODS: `toggle`. 140 | */ 141 | it("should toggle elements", () => { 142 | let store: EStore = new EStore(todosFactory()); 143 | let todoOrNotTodo = new Todo(false, "This is not in the store", '1'); 144 | store.toggle(todoOrNotTodo); 145 | expect(store.entries.size).toEqual(3); 146 | expect(store.contains(todoOrNotTodo)).toBeTruthy(); 147 | store.toggle(todoOrNotTodo); 148 | expect(store.entries.size).toEqual(2); 149 | expect(store.contains(todoOrNotTodo)).toBeFalsy(); 150 | store.toggle(todoOrNotTodo); 151 | expect(store.entries.size).toEqual(3); 152 | expect(store.contains(todoOrNotTodo)).toBeTruthy(); 153 | }); 154 | 155 | /** 156 | * CONCERN: Utility API 157 | * METHODS: `contains`. 158 | */ 159 | it("should return true when the store contains the entity and false otherwise", () => { 160 | let todos = todosFactory(); 161 | let todo0 = todos[0]; 162 | let todo1 = todos[1]; 163 | let todoOrNotTodo = new Todo(false, "This is not in the store", '1'); 164 | 165 | let store: EStore = new EStore(todos); 166 | expect(store.contains(todo0)).toBeTruthy(); 167 | expect(store.contains(todo1)).toBeTruthy(); 168 | expect(store.contains(todo0.gid!)).toBeTruthy(); 169 | expect(store.contains(todo1.gid!)).toBeTruthy(); 170 | expect(store.contains(todoOrNotTodo)).toBeFalsy(); 171 | expect(store.contains(todoOrNotTodo.gid!)).toBeFalsy(); 172 | }); 173 | 174 | /** 175 | * CONCERN: Utility API 176 | * METHODS: `containsByID`. 177 | */ 178 | it("should return true when the store contains the entity and false otherwise", () => { 179 | let todoByID = new Todo(false, "This is not in the store", undefined, '1'); 180 | let todoByIDAlso = new Todo(false, "This is not in the store", undefined, '2'); 181 | let store: EStore = new EStore(); 182 | store.post(todoByID); 183 | expect(store.containsById(todoByID)).toBeTruthy(); 184 | expect(store.containsById(todoByIDAlso)).toBeFalsy(); 185 | }); 186 | 187 | 188 | /** 189 | * CONCERN: Live Count 190 | * METHODS: `count`. 191 | */ 192 | it("should return an observable count", (done) => { 193 | let store: EStore = new EStore(todosFactory()); 194 | store.count().subscribe(c => { 195 | expect(c).toEqual(2); 196 | done() 197 | }); 198 | }); 199 | 200 | /** 201 | * CONCERN: Deleting Entities 202 | * METHODS: `delete`, `deleteN`, `deleteA, deleteP`. 203 | * DESIGN CONSIDERATIONS: 204 | * Deletion should occur across slices, 205 | * id entries (We track entities by gid and id), 206 | * and active state as well. 207 | */ 208 | it("should delete the element", () => { 209 | let todo1 = new Todo(false, "This is not in the store", GUID(), '1'); 210 | let todo2 = new Todo(true, "This is not in the store", GUID(), '2'); 211 | let todos = [todo1, todo2]; 212 | let store: EStore = new EStore(todos); 213 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 214 | let todo = todos[1]; 215 | store.addActive(todo); 216 | expect(todo.complete).toBeTruthy(); 217 | expect(store.getSlice(TodoSliceEnum.COMPLETE)!.contains(todo)).toBeTruthy(); 218 | expect(store.idEntries.size).toEqual(2); 219 | store.delete(todo); 220 | //The slice no longer contains todo 221 | expect(store.contains(todo.gid!)).toBeFalsy(); 222 | expect(values(store.allSnapshot()).length).toEqual(1); 223 | let slice: Slice = store.getSlice(TodoSliceEnum.COMPLETE)!; 224 | expect(slice.findOne(todo.gid!)).toBeUndefined(); 225 | expect(slice.contains(todo.gid!)).toBeFalsy(); 226 | //The todo is not in active state 227 | expect(store.active.get(todo.gid!)).toBeUndefined(); 228 | //The todo should be deleted from the `idEntries` array 229 | expect(store.idEntries.size).toEqual(1); 230 | //deleteA 231 | store.reset(); 232 | expect(store.isEmpty()).toBeTruthy(); 233 | store.postA(todos); 234 | store.deleteA(todos); 235 | expect(store.isEmpty()).toBeTruthy(); 236 | expect(values(store.idEntries).length).toEqual(0); 237 | //deleteN 238 | store.reset(); 239 | expect(store.isEmpty()).toBeTruthy(); 240 | store.postA(todos); 241 | expect(store.idEntries.size).toEqual(2); 242 | store.deleteN(...todos); 243 | expect(store.isEmptySnapshot()).toBeTruthy(); 244 | expect(store.idEntries.size).toEqual(0); 245 | //deleteP 246 | store.reset(); 247 | expect(store.isEmptySnapshot()).toBeTruthy(); 248 | store.postA(todos); 249 | expect(store.idEntries.size).toEqual(2); 250 | store.deleteP((e: Todo) => !e.complete); 251 | expect(store.idEntries.size).toEqual(1); 252 | expect(store.isEmptySnapshot()).toBeFalsy(); 253 | }); 254 | 255 | /** 256 | * CONCERN: Entity Equality 257 | * METHODS: `equalityByGUID` and `equalityByID`. 258 | */ 259 | it("should show that two entities are using both equalsByGUID and equalsByID", () => { 260 | const guid = GUID(); 261 | let todoOrNotTodo1 = new Todo(false, "This is not in the store", guid, '1'); 262 | let todoOrNotTodo2 = new Todo(false, "This is not in the store", guid, '1'); 263 | 264 | let store: EStore = new EStore(); 265 | store.post(todoOrNotTodo1); 266 | store.post(todoOrNotTodo2); 267 | expect(todoOrNotTodo1.gid).toEqual(guid); 268 | expect(store.equalsByGUID(todoOrNotTodo1, todoOrNotTodo2)).toBeTruthy(); 269 | expect(store.equalsByID(todoOrNotTodo1, todoOrNotTodo2)).toBeTruthy(); 270 | }); 271 | 272 | 273 | 274 | /** 275 | * CONCERN: Utility API 276 | * METHODS: `findOneByID`. 277 | */ 278 | it("should add a slice to the store be created with 1 complete todo element", () => { 279 | let store: EStore = new EStore(todosFactory()); 280 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 281 | expect( 282 | store.getSlice(TodoSliceEnum.COMPLETE)!.entries.size 283 | ).toEqual(1); 284 | }); 285 | 286 | /** 287 | * CONCERN: Live filtering 288 | * METHODS: `getSlice` and `removeSlice` 289 | * DESIGN CONSIDERATIONS: 290 | * Removing a Slice does not remove entities from the cenral store. 291 | */ 292 | 293 | it("should have the right slice count", (done) => { 294 | let store: EStore = new EStore(); 295 | store.postA(todosFactory()); 296 | expect(store.isEmptySnapshot()).toBeFalsy(); 297 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 298 | let slice = store.getSlice(TodoSliceEnum.COMPLETE)!; 299 | expect(slice.isEmptySnapshot()).toBeFalsy(); 300 | slice.count() 301 | .subscribe(c => { 302 | expect(c).toEqual(1); 303 | done() 304 | }); 305 | //Remove the slice 306 | store.removeSlice(TodoSliceEnum.COMPLETE); 307 | expect(store.getSlice(TodoSliceEnum.COMPLETE)).toBeUndefined(); 308 | //No entries were deleted from the store. 309 | expect(store.entries.size).toEqual(2); 310 | 311 | //should have 2 completed slice elements 312 | store.reset(); 313 | store.postA(todosFactory()); 314 | 315 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 316 | store.post(new Todo(true, "You had me at hello!")); 317 | 318 | expect(store.entries.size).toEqual(3); 319 | expect( 320 | store.getSlice(TodoSliceEnum.COMPLETE)!. 321 | entries.size).toEqual(2) 322 | }, 1500); 323 | 324 | 325 | 326 | /** 327 | * METHODS: isEmpty() 328 | * DESIGN CONSIDERATIONS 329 | * `isEmpty` and `count` should both 330 | * work on slices as well. 331 | */ 332 | it("should reflect the stores is empty", (done) => { 333 | let store: EStore = new EStore(); 334 | store.isEmpty().subscribe(empty => { 335 | expect(empty).toBeTruthy(); 336 | done() 337 | }); 338 | }); 339 | 340 | it("should reflect the count is zero", (done) => { 341 | let store: EStore = new EStore(); 342 | store.count().subscribe(c => { 343 | expect(c).toEqual(0); 344 | done() 345 | }); 346 | }); 347 | 348 | 349 | it("should have an empty slice", (done) => { 350 | let store: EStore = new EStore(); 351 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 352 | const empty:Observable = store.getSlice(TodoSliceEnum.COMPLETE)!.isEmpty(); 353 | empty.subscribe(e=>{ 354 | expect(e).toBeTruthy() 355 | done() 356 | }) 357 | }); 358 | 359 | it("should have a zero slice count", (done) => { 360 | let store: EStore = new EStore(); 361 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 362 | store 363 | .getSlice(TodoSliceEnum.COMPLETE)! 364 | .count() 365 | .subscribe(c => { 366 | expect(c).toEqual(0); 367 | done() 368 | }); 369 | }); 370 | 371 | it("should not have an empty store", (done) => { 372 | let store: EStore = new EStore(); 373 | store.postA(todosFactory()); 374 | 375 | store.isEmpty().subscribe(empty => { 376 | expect(empty).toBeFalsy(); 377 | done() 378 | }); 379 | }); 380 | 381 | it("should not have an updated store count", (done) => { 382 | let store: EStore = new EStore(); 383 | store.postA(todosFactory()); 384 | 385 | store.count().subscribe(c => { 386 | expect(c).toEqual(2); 387 | done() 388 | }); 389 | }); 390 | 391 | it("should not have an updated store predicate count", (done) => { 392 | let store: EStore = new EStore(); 393 | store.postA(todosFactory()); 394 | 395 | store.count(todo => todo.complete).subscribe(c => { 396 | expect(c).toEqual(1); 397 | done() 398 | }); 399 | }); 400 | 401 | it("should not have an empty slice", (done) => { 402 | let store: EStore = new EStore(); 403 | store.postA(todosFactory()); 404 | 405 | store.addSlice(todo => todo.complete, TodoSliceEnum.COMPLETE); 406 | let s: Slice = store.getSlice(TodoSliceEnum.COMPLETE)!; 407 | s.isEmpty().subscribe(empty => { 408 | expect(empty).toBeFalsy(); 409 | done() 410 | }); 411 | }); -------------------------------------------------------------------------------- /projects/slice/src/lib/EStore.ts: -------------------------------------------------------------------------------- 1 | import { AbstractStore } from './AbstractStore'; 2 | import { StoreConfig } from './models/StoreConfig'; 3 | import { GUID } from './utilities'; 4 | 5 | import { ActionTypes, Predicate, Delta } from './models/'; 6 | import { ReplaySubject, of, Observable, combineLatest } from 'rxjs'; 7 | import { takeWhile, filter, switchMap } from 'rxjs/operators'; 8 | import { Slice } from './Slice'; 9 | 10 | /** 11 | * This `todoFactory` code will be used to illustrate the API examples. The following 12 | * utilities are used in the tests and the API Typedoc examples contained here. 13 | * @example Utilities for API Examples 14 | * ``` 15 | * export const enum TodoSliceEnum { 16 | * COMPLETE = "Complete", 17 | * INCOMPLETE = "Incomplete" 18 | * } 19 | * export class Todo { 20 | * constructor( 21 | * public complete: boolean, 22 | * public title: string, 23 | * public gid?:string, 24 | * public id?:string) {} 25 | * } 26 | * 27 | * export let todos = [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")]; 28 | * 29 | * export function todosFactory():Todo[] { 30 | * return [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")]; 31 | * } 32 | * ``` 33 | */ 34 | export class EStore extends AbstractStore { 35 | /** 36 | * Store constructor (Initialization with element is optional) 37 | * 38 | * perform initial notification to all observers, 39 | * such that functions like {@link combineLatest}{} 40 | * will execute at least once. 41 | * 42 | * @param entities The entities to initialize the store with. 43 | * @param config The optional configuration instance. 44 | * 45 | * @example EStore Creation 46 | * ``` 47 | * // Initialize the Store 48 | * let store: EStore = new EStore(todosFactory()); 49 | * ``` 50 | */ 51 | constructor(entities: E[] = [], config?: StoreConfig) { 52 | super(config); 53 | const delta: Delta = { type: ActionTypes.INTIALIZE, entries: entities }; 54 | this.post(entities); 55 | this.notifyDelta.next(delta); 56 | } 57 | 58 | /** 59 | * Calls complete on all EStore {@link ReplaySubject} instances. 60 | * 61 | * Call destroy when disposing of the store. 62 | */ 63 | override destroy() { 64 | super.destroy(); 65 | this.notifyLoading.complete(); 66 | this.notifyActive.complete(); 67 | this.slices.forEach((slice) => slice.destroy()); 68 | } 69 | 70 | /** 71 | * Toggles the entity: 72 | * 73 | * If the store contains the entity 74 | * it will be deleted. If the store 75 | * does not contains the entity, 76 | * it is added. 77 | * @param e The entity to toggle 78 | * @example Toggle the Todo instance 79 | * ``` 80 | * estore.post(todo); 81 | * // Remove todo 82 | * estore.toggle(todo); 83 | * // Add it back 84 | * estore.toggle(todo); 85 | * ``` 86 | */ 87 | public toggle(e: E) { 88 | if (this.contains(e)) { 89 | this.delete(e); 90 | } else { 91 | this.post(e); 92 | } 93 | } 94 | 95 | /** 96 | * Notifies observers when the store is empty. 97 | */ 98 | private notifyActive = new ReplaySubject>(1); 99 | 100 | /** 101 | * `Map` of active entties. The instance is public and can be used 102 | * directly to add and remove active entities, however we recommend 103 | * using the {@link addActive} and {@link deleteActive} methods. 104 | */ 105 | public active: Map = new Map(); 106 | 107 | /** 108 | * Add multiple entity entities to active. 109 | * 110 | * If the entity is not contained in the store it is added 111 | * to the store before it is added to `active`. 112 | * 113 | * Also we clone the map prior to broadcasting it with 114 | * `notifyActive` to make sure we will trigger Angular 115 | * change detection in the event that it maintains 116 | * a reference to the `active` state `Map` instance. 117 | * 118 | * @example Add todo1 and todo2 as active 119 | * ``` 120 | * addActive(todo1); 121 | * addActive(todo2); 122 | * ``` 123 | */ 124 | public addActive(e: E) { 125 | if (this.contains(e)) { 126 | this.active.set((e).gid, e); 127 | this.notifyActive.next(new Map(this.active)); 128 | } else { 129 | this.post(e); 130 | this.active.set((e).gid, e); 131 | this.notifyActive.next(new Map(this.active)); 132 | } 133 | } 134 | 135 | /** 136 | * Delete an active entity. 137 | * 138 | * Also we clone the map prior to broadcasting it with 139 | * `notifyActive` to make sure we will trigger Angular 140 | * change detection in the event that it maintains 141 | * a reference to the `active` state `Map` instance. 142 | * 143 | * @example Remove todo1 and todo2 as active entities 144 | * ``` 145 | * deleteActive(todo1); 146 | * deleteActive(todo2); 147 | * ``` 148 | */ 149 | public deleteActive(e: E) { 150 | this.active.delete((e).gid); 151 | this.notifyActive.next(new Map(this.active)); 152 | } 153 | 154 | /** 155 | * Clear / reset the active entity map. 156 | * 157 | * Also we clone the map prior to broadcasting it with 158 | * `notifyActive` to make sure we will trigger Angular 159 | * change detection in the event that it maintains 160 | * a reference to the `active` state `Map` instance. 161 | * 162 | * @example Clear active todo instances 163 | * ``` 164 | * store.clearActive(); 165 | * ``` 166 | */ 167 | clearActive() { 168 | this.active.clear(); 169 | this.notifyActive.next(new Map(this.active)); 170 | } 171 | 172 | /** 173 | * Observe the active entities. 174 | * 175 | * @example 176 | * ``` 177 | * let active$ = store.observeActive(); 178 | * ``` 179 | */ 180 | public observeActive() { 181 | return this.notifyActive.asObservable(); 182 | } 183 | 184 | /** 185 | * Observe the active entity. 186 | * @example 187 |
188 |     let active$ = source.activeSnapshot();
189 |     
190 | */ 191 | public activeSnapshot() { 192 | return Array.from(this.active.values()); 193 | } 194 | 195 | //================================================ 196 | // LOADING 197 | //================================================ 198 | 199 | /** 200 | * Observable of errors occurred during a load request. 201 | * 202 | * The error Observable should be created by the 203 | * client. 204 | */ 205 | public loadingError!: Observable; 206 | 207 | /** 208 | * Notifies observers when the store is loading. 209 | * 210 | * This is a common pattern found when implementing 211 | * `Observable` data sources. 212 | */ 213 | private notifyLoading = new ReplaySubject(1); 214 | 215 | /** 216 | * The current loading state. Use loading when fetching new 217 | * data for the store. The default loading state is `true`. 218 | * 219 | * This is such that if data is fetched asynchronously 220 | * in a service, components can wait on loading notification 221 | * before attempting to retrieve data from the service. 222 | * 223 | * Loading could be based on a composite response. For example 224 | * when the stock and mutual funds have loaded, set loading to `false`. 225 | */ 226 | private _loading: boolean = true; 227 | 228 | /** 229 | * Sets the current loading state and notifies observers. 230 | */ 231 | set loading(loading: boolean) { 232 | this._loading = loading; 233 | this.notifyLoading.next(this._loading); 234 | } 235 | 236 | /** 237 | * @return A snapshot of the loading state. 238 | * @example Create a reference to the loading state 239 | * ``` 240 | * const loading:boolean = todoStore.loading; 241 | * ``` 242 | */ 243 | get loading() { 244 | return this._loading; 245 | } 246 | 247 | /** 248 | * Observe loading. 249 | * 250 | * Note that this obverable piped through 251 | * `takeWhile(v->v, true), such that it will 252 | * complete after each emission. 253 | * 254 | * See: 255 | * https://fireflysemantics.medium.com/waiting-on-estore-to-load-8dcbe161613c 256 | * 257 | * For more details. 258 | * Also note that v=>v is the same as v=>v!=false 259 | * 260 | * @example 261 | * ``` 262 | * const observeLoadingHandler: Observer = { 263 | * complete: () => { 264 | * console.log(`Data Loaded and Observable Marked as Complete`); 265 | * }, // completeHandler 266 | * error: () => { 267 | * console.log(`Any Errors?`); 268 | * }, // errorHandler 269 | * next: (l) => { 270 | * console.log(`Data loaded and loading is ${l}`); 271 | * }, 272 | * }; 273 | * 274 | * const observeLoadingResubscribeHandler: Observer = { 275 | * complete: () => { 276 | * console.log(`Data Loaded and Resubscribe Observable Marked as Complete`); 277 | * }, // completeHandler 278 | * error: () => { 279 | * console.log(`Any Resubscribe Errors?`); 280 | * }, // errorHandler 281 | * next: (l) => { 282 | * console.log(`Data loaded and resusbscribe loading value is ${l}`); 283 | * }, 284 | * }; 285 | * 286 | * const todoStore: EStore = new EStore(); 287 | * //============================================ 288 | * // Loading is true by default 289 | * //============================================ 290 | * console.log(`The initial value of loading is ${todoStore.loading}`); 291 | * //============================================ 292 | * // Observe Loading 293 | * //============================================ 294 | * let loading$: Observable = todoStore.observeLoading(); 295 | * loading$.subscribe((l) => console.log(`The value of loading is ${l}`)); 296 | * 297 | * todoStore.loading = false; 298 | * loading$.subscribe(observeLoadingHandler); 299 | * //============================================ 300 | * // The subscription no longer fires 301 | * //============================================ 302 | * todoStore.loading = true; 303 | * todoStore.loading = false; 304 | * 305 | * //============================================ 306 | * // The subscription no longer fires, 307 | * // so if we want to observe loading again 308 | * // resusbscribe. 309 | * //============================================ 310 | * todoStore.loading = true; 311 | * loading$ = todoStore.observeLoading(); 312 | * loading$.subscribe(observeLoadingResubscribeHandler); 313 | * todoStore.loading = false; 314 | * ``` 315 | */ 316 | public observeLoading() { 317 | return this.notifyLoading.asObservable().pipe(takeWhile((v) => v, true)); 318 | } 319 | 320 | /** 321 | * Notfiies when loading has completed. 322 | */ 323 | public observeLoadingComplete() { 324 | return this.observeLoading().pipe( 325 | filter((loading) => loading == false), 326 | switchMap(() => of(true)) 327 | ); 328 | } 329 | 330 | //================================================ 331 | // SEARCHING 332 | //================================================ 333 | /** 334 | * Observable of errors occurred during a search request. 335 | * 336 | * The error Observable should be created by the 337 | * client. 338 | */ 339 | public searchError!: Observable; 340 | 341 | /** 342 | * Notifies observers that a search is in progress. 343 | * 344 | * This is a common pattern found when implementing 345 | * `Observable` data sources. 346 | */ 347 | private notifySearching = new ReplaySubject(1); 348 | 349 | /** 350 | * The current `searching` state. Use `searching` 351 | * for example to display a spinnner 352 | * when performing a search. 353 | * The default `searching` state is `false`. 354 | */ 355 | private _searching: boolean = false; 356 | 357 | /** 358 | * Sets the current searching state and notifies observers. 359 | */ 360 | set searching(searching: boolean) { 361 | this._searching = searching; 362 | this.notifySearching.next(this._searching); 363 | } 364 | 365 | /** 366 | * @return A snapshot of the searching state. 367 | */ 368 | get searching() { 369 | return this._searching; 370 | } 371 | 372 | /** 373 | * Observe searching. 374 | * @example 375 |
376 |     let searching$ = source.observeSearching();
377 |     
378 | 379 | Note that this obverable piped through 380 | `takeWhile(v->v, true), such that it will 381 | complete after each emission. 382 | 383 | See: 384 | https://medium.com/@ole.ersoy/waiting-on-estore-to-load-8dcbe161613c 385 | 386 | For more details. 387 | */ 388 | public observeSearching(): Observable { 389 | return this.notifySearching.asObservable().pipe(takeWhile((v) => v, true)); 390 | } 391 | 392 | /** 393 | * Notfiies when searching has completed. 394 | */ 395 | public observeSearchingComplete(): Observable { 396 | return this.observeSearching().pipe( 397 | filter((searching) => searching == false), 398 | switchMap(() => of(true)) 399 | ); 400 | } 401 | 402 | /** 403 | * Store slices 404 | */ 405 | private slices: Map> = new Map(); 406 | 407 | /** 408 | * Adds a slice to the store and keys it by the slices label. 409 | * 410 | * @param p 411 | * @param label 412 | * 413 | * @example Setup a Todo Slice for COMPLETE Todos 414 | ``` 415 | source.addSlice(todo => todo.complete, TodoSlices.COMPLETE); 416 | ``` 417 | */ 418 | addSlice(p: Predicate, label: string) { 419 | const slice: Slice = new Slice(label, p, this); 420 | this.slices.set(slice.label, slice); 421 | } 422 | 423 | /** 424 | * Remove a slice 425 | * @param label The label identifying the slice 426 | * 427 | * @example Remove the TodoSlices.COMPLETE Slice 428 | ``` 429 | source.removeSlice(TodoSlices.COMPLETE); 430 | ``` 431 | */ 432 | removeSlice(label: string) { 433 | this.slices.delete(label); 434 | } 435 | 436 | /** 437 | * Get a slice 438 | * @param label The label identifying the slice 439 | * @return The Slice instance or undefined 440 | * 441 | * @example Get the TodoSlices.COMPLETE slice 442 | ``` 443 | source.getSlice(TodoSlices.COMPLETE); 444 | ``` 445 | */ 446 | getSlice(label: string): Slice | undefined { 447 | return this.slices.get(label); 448 | } 449 | 450 | /** 451 | * Post (Add a new) element(s) to the store. 452 | * @param e An indiidual entity or an array of entities 453 | * @example Post a Todo instance. 454 | * 455 | *``` 456 | * store.post(todo); 457 | *``` 458 | */ 459 | post(e: E | E[]) { 460 | if (!Array.isArray(e)) { 461 | const guid: string = (e)[this.GUID_KEY] 462 | ? (e)[this.GUID_KEY] 463 | : GUID(); 464 | (e)[this.GUID_KEY] = guid; 465 | this.entries.set(guid, e); 466 | this.updateIDEntry(e); 467 | Array.from(this.slices.values()).forEach((s) => { 468 | s.post(e); 469 | }); 470 | //Create a new array reference to trigger Angular change detection. 471 | let v: E[] = [...Array.from(this.entries.values())]; 472 | const delta: Delta = { type: ActionTypes.POST, entries: [e] }; 473 | this.notifyAll(v, delta); 474 | } else { 475 | this.postA(e); 476 | } 477 | } 478 | 479 | /** 480 | * Post N entities to the store. 481 | * @param ...e 482 | * @example Post two Todo instances. 483 | * ``` 484 | * store.post(todo1, todo2); 485 | * ``` 486 | */ 487 | postN(...e: E[]) { 488 | e.forEach((e) => { 489 | const guid: string = (e)[this.GUID_KEY] 490 | ? (e)[this.GUID_KEY] 491 | : GUID(); 492 | (e)[this.GUID_KEY] = guid; 493 | this.entries.set(guid, e); 494 | this.updateIDEntry(e); 495 | }); 496 | Array.from(this.slices.values()).forEach((s) => { 497 | s.postA(e); 498 | }); 499 | //Create a new array reference to trigger Angular change detection. 500 | let v: E[] = [...Array.from(this.entries.values())]; 501 | const delta: Delta = { type: ActionTypes.POST, entries: e }; 502 | this.notifyAll(v, delta); 503 | } 504 | 505 | /** 506 | * Post (Add) an array of elements to the store. 507 | * @param e 508 | * @example Post a Todo array. 509 | * 510 | * ``` 511 | * store.post([todo1, todo2]); 512 | * ``` 513 | */ 514 | postA(e: E[]) { 515 | this.postN(...e); 516 | } 517 | 518 | /** 519 | * Put (Update) an entity. 520 | * @param e 521 | * @example Put a Todo instance. 522 | * ``` 523 | * store.put(todo1); 524 | * ``` 525 | */ 526 | put(e: E | E[]) { 527 | if (!Array.isArray(e)) { 528 | let id: string = (e)[this.GUID_KEY]; 529 | this.entries.set(id, e); 530 | this.updateIDEntry(e); 531 | let v: E[] = [...Array.from(this.entries.values())]; 532 | this.notify.next(v); 533 | const delta: Delta = { type: ActionTypes.PUT, entries: [e] }; 534 | this.notifyDelta.next(delta); 535 | Array.from(this.slices.values()).forEach((s) => { 536 | s.put(e); 537 | }); 538 | } else { 539 | this.putA(e); 540 | } 541 | } 542 | 543 | /** 544 | * Put (Update) an element or add an element that was read from a persistence source 545 | * and thus already has an assigned global id`. 546 | * @param e The enetity instances to update. 547 | * @example Put N Todo instances. 548 | * 549 | * ``` 550 | * store.put(todo1, todo2); 551 | * ``` 552 | */ 553 | putN(...e: E[]) { 554 | this.putA(e); 555 | } 556 | 557 | /** 558 | * Put (Update) the array of enntities. 559 | * @param e The array of enntities to update 560 | * @example Put an array of Todo instances. 561 | * ``` 562 | * store.put([todo1, todo2]); 563 | * ``` 564 | */ 565 | putA(e: E[]) { 566 | e.forEach((e) => { 567 | let guid: string = (e)[this.GUID_KEY]; 568 | this.entries.set(guid, e); 569 | this.updateIDEntry(e); 570 | }); 571 | //Create a new array reference to trigger Angular change detection. 572 | let v: E[] = [...Array.from(this.entries.values())]; 573 | this.notify.next(v); 574 | const delta: Delta = { type: ActionTypes.PUT, entries: e }; 575 | this.notifyDelta.next(delta); 576 | Array.from(this.slices.values()).forEach((s) => { 577 | s.putA(e); 578 | }); 579 | } 580 | 581 | /** 582 | * Delete (Update) the array of elements. 583 | * @param e 584 | * @example Delete todo1. 585 | * ``` 586 | * store.delete(todo1]); 587 | * ``` 588 | */ 589 | delete(e: E | E[]) { 590 | if (!Array.isArray(e)) { 591 | this.deleteActive(e); 592 | const guid = (e)[this.GUID_KEY]; 593 | this.entries.delete(guid); 594 | this.deleteIDEntry(e); 595 | Array.from(this.slices.values()).forEach((s) => { 596 | s.entries.delete(guid); 597 | }); 598 | //Create a new array reference to trigger Angular change detection. 599 | let v: E[] = [...Array.from(this.entries.values())]; 600 | const delta: Delta = { type: ActionTypes.DELETE, entries: [e] }; 601 | this.notifyAll(v, delta); 602 | Array.from(this.slices.values()).forEach((s) => { 603 | s.delete(e); 604 | }); 605 | } else { 606 | this.deleteA(e); 607 | } 608 | } 609 | 610 | /** 611 | * Delete N elements. 612 | * @param ...e 613 | * @example Delete N Todo instance argument. 614 | * ``` 615 | * store.deleteN(todo1, todo2); 616 | * ``` 617 | */ 618 | deleteN(...e: E[]) { 619 | this.deleteA(e); 620 | } 621 | 622 | /** 623 | * Delete an array of elements. 624 | * @param e The array of instances to be deleted 625 | * @example Delete the array of Todo instances. 626 | * ``` 627 | * store.deleteA([todo1, todo2]); 628 | * ``` 629 | */ 630 | deleteA(e: E[]) { 631 | e.forEach((e) => { 632 | this.deleteActive(e); 633 | const guid = (e)[this.GUID_KEY]; 634 | this.entries.delete(guid); 635 | this.deleteIDEntry(e); 636 | Array.from(this.slices.values()).forEach((s) => { 637 | s.entries.delete(guid); 638 | }); 639 | }); 640 | //Create a new array reference to trigger Angular change detection. 641 | let v: E[] = [...Array.from(this.entries.values())]; 642 | const delta: Delta = { type: ActionTypes.DELETE, entries: e }; 643 | this.notifyAll(v, delta); 644 | Array.from(this.slices.values()).forEach((s) => { 645 | s.deleteA(e); 646 | }); 647 | } 648 | 649 | /** 650 | * Delete elements by {@link Predicate}. 651 | * @param p The predicate. 652 | * @example Delete the Todo instances. 653 | * ``` 654 | * store.delete(todo1, todo2); 655 | * ``` 656 | */ 657 | deleteP(p: Predicate) { 658 | const d: E[] = []; 659 | Array.from(this.entries.values()).forEach((e) => { 660 | if (p(e)) { 661 | d.push(e); 662 | const id = (e)[this.GUID_KEY]; 663 | this.entries.delete(id); 664 | this.deleteActive(e); 665 | this.deleteIDEntry(e); 666 | } 667 | }); 668 | //Create a new array reference to trigger Angular change detection. 669 | let v: E[] = [...Array.from(this.entries.values())]; 670 | const delta: Delta = { type: ActionTypes.DELETE, entries: d }; 671 | this.notifyAll(v, delta); 672 | Array.from(this.slices.values()).forEach((s) => { 673 | s.deleteA(d); 674 | }); 675 | } 676 | 677 | /** 678 | * If the entity has the `id` key initialized with a value, 679 | * then also add the entity to the `idEntries`. 680 | * 681 | * @param e The element to be added to the `idEntries`. 682 | */ 683 | private updateIDEntry(e: E) { 684 | if ((e)[this.ID_KEY]) { 685 | this.idEntries.set((e)[this.ID_KEY], e); 686 | } 687 | } 688 | 689 | /** 690 | * If the entity has the `id` key initialized with a value, 691 | * then also delete the entity to the `idEntries`. 692 | * 693 | * @param e The element to be added to the `idEntries`. 694 | */ 695 | private deleteIDEntry(e: E) { 696 | if ((e)[this.ID_KEY]) { 697 | this.idEntries.delete((e)[this.ID_KEY]); 698 | } 699 | } 700 | 701 | /** 702 | * Resets the store and all contained slice instances to empty. 703 | * Also perform delta notification that sends all current store entries. 704 | * The ActionType.RESET code is sent with the delta notification. Slices 705 | * send their own delta notification. 706 | * 707 | * @example Reset the store. 708 | * ``` 709 | * store.reset(); 710 | * ``` 711 | */ 712 | reset() { 713 | const delta: Delta = { 714 | type: ActionTypes.RESET, 715 | entries: Array.from(this.entries.values()), 716 | }; 717 | this.notifyAll([], delta); 718 | this.entries = new Map(); 719 | Array.from(this.slices.values()).forEach((s) => { 720 | s.reset(); 721 | }); 722 | } 723 | 724 | /** 725 | * Call all the notifiers at once. 726 | * 727 | * @param v 728 | * @param delta 729 | */ 730 | protected override notifyAll(v: E[], delta: Delta) { 731 | super.notifyAll(v, delta); 732 | this.notifyLoading.next(this.loading); 733 | } 734 | } 735 | -------------------------------------------------------------------------------- /projects/slice/src/lib/OStore.spec.ts: -------------------------------------------------------------------------------- 1 | import { KeyObsValueReset, ObsValueReset, OStore, OStoreStart } from './'; 2 | 3 | const START: OStoreStart = { 4 | K1: { value: 'V1', reset: 'ResetValue' }, 5 | }; 6 | interface ISTART extends KeyObsValueReset { 7 | K1: ObsValueReset; 8 | } 9 | let OS: OStore; 10 | 11 | describe('Object Store initialization', () => { 12 | beforeEach(() => { 13 | OS = new OStore(START); 14 | }); 15 | 16 | it('should have V1 as the store value on initialization', (done) => { 17 | expect(OS.snapshot(OS.S.K1)).toEqual('V1'); 18 | OS.S.K1.obs?.subscribe((v) => { 19 | expect(v).toEqual('V1'); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('Object Store Reset', () => { 26 | beforeEach(() => { 27 | OS = new OStore(START); 28 | }); 29 | it('should have ResetValue as the value when the store is reset', (done) => { 30 | OS.reset(); 31 | expect(OS.snapshot(OS.S.K1)).toEqual('ResetValue'); 32 | OS.S.K1.obs?.subscribe((v) => { 33 | expect(v).toEqual('ResetValue'); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | describe('Object Store PUT API', () => { 39 | beforeEach(() => { 40 | OS = new OStore(START); 41 | }); 42 | it('should update the value after a put=', (done) => { 43 | const value = 'NewValue'; 44 | OS.put(OS.S.K1, value); 45 | expect(OS.snapshot(OS.S.K1)).toEqual(value); 46 | OS.S.K1.obs?.subscribe((v) => { 47 | expect(v).toEqual(value); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('Object Store DELETE API', () => { 54 | beforeEach(() => { 55 | OS = new OStore(START); 56 | }); 57 | it('should delte a value after a delete', (done) => { 58 | OS.delete(OS.S.K1); 59 | expect(OS.snapshot(OS.S.K1)).toBeUndefined(); 60 | OS.S.K1.obs?.subscribe((v) => { 61 | expect(v).toBeUndefined(); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('Object Store Clear API', () => { 68 | beforeEach(() => { 69 | OS = new OStore(START); 70 | }); 71 | it('should delte a value after a delete', (done) => { 72 | OS.clear(); 73 | expect(OS.snapshot(OS.S.K1)).toBeUndefined(); 74 | OS.S.K1.obs?.subscribe((v) => { 75 | expect(v).toBeUndefined(); 76 | done(); 77 | }); 78 | }); 79 | }); 80 | 81 | 82 | describe('Object Store Count API', () => { 83 | beforeEach(() => { 84 | OS = new OStore(START); 85 | }); 86 | it('should return true when a value does exists for a given key', () => { 87 | expect(OS.count()).toEqual(1); 88 | }); 89 | }); 90 | 91 | 92 | describe('Object Store Exists API', () => { 93 | beforeEach(() => { 94 | OS = new OStore(START); 95 | }); 96 | it('should return true when a value does exists for a given key', () => { 97 | expect(OS.exists(OS.S.K1)).toBeTrue(); 98 | }); 99 | it('should return false when a value does not exist for a given key', () => { 100 | expect(OS.exists('DOES_NOT_EXIST')).toBeFalse(); 101 | }); 102 | }); 103 | 104 | describe('Object Store isEmpty API', () => { 105 | beforeEach(() => { 106 | OS = new OStore(START); 107 | }); 108 | it('should return false when is empty is called on an initialized store', () => { 109 | expect(OS.isEmpty()).toBeFalse(); 110 | }); 111 | }); 112 | 113 | 114 | -------------------------------------------------------------------------------- /projects/slice/src/lib/OStore.ts: -------------------------------------------------------------------------------- 1 | import { ReplaySubject, Observable } from 'rxjs'; 2 | 3 | /** 4 | * Initialize hte store with this. 5 | */ 6 | export interface ValueReset { 7 | value: any; 8 | reset?: any; 9 | } 10 | 11 | /** 12 | * OStore Key Value Reset 13 | */ 14 | export interface ObsValueReset { 15 | value: any; 16 | reset?: any; 17 | obs: Observable; 18 | } 19 | 20 | export interface KeyObsValueReset { 21 | [key: string]: ObsValueReset; 22 | } 23 | 24 | export interface OStoreStart { 25 | [key: string]: ValueReset; 26 | } 27 | 28 | export class OStore { 29 | /** 30 | * Start keys and values 31 | * passed in via constructor. 32 | */ 33 | public S!: E; 34 | 35 | constructor(start: OStoreStart) { 36 | if (start) { 37 | this.S = start; 38 | const keys = Object.keys(start); 39 | keys.forEach((k) => { 40 | const ovr = start[k] as ObsValueReset; 41 | this.post(ovr, ovr.value); 42 | ovr.obs = this.observe(ovr)!; 43 | }); 44 | } 45 | } 46 | 47 | /** 48 | * Reset the state of the OStore to the 49 | * values or reset provided in the constructor 50 | * {@link OStoreStart} instance. 51 | */ 52 | public reset() { 53 | if (this.S) { 54 | const keys = Object.keys(this.S); 55 | keys.forEach((k) => { 56 | const ovr: ObsValueReset = this.S[k]; 57 | this.put(ovr, ovr.reset ? ovr.reset : ovr.value); 58 | }); 59 | } 60 | } 61 | 62 | /** 63 | * Map of Key Value pair entries 64 | * containing values store in this store. 65 | */ 66 | public entries: Map = new Map(); 67 | 68 | /** 69 | * Map of replay subject id to `ReplaySubject` instance. 70 | */ 71 | private subjects: Map> = new Map(); 72 | 73 | /** 74 | * Set create a key value pair entry and creates a 75 | * corresponding replay subject instance that will 76 | * be used to broadcast updates. 77 | * 78 | * @param key The key identifying the value 79 | * @param value The value 80 | */ 81 | private post(key: ObsValueReset, value: any) { 82 | this.entries.set(key, value); 83 | this.subjects.set(key, new ReplaySubject(1)); 84 | //Emit immediately so that Observers can receive 85 | //the value straight away. 86 | const subject = this.subjects.get(key); 87 | if (subject) { 88 | subject.next(value); 89 | } 90 | } 91 | /** 92 | * Update a value and notify subscribers. 93 | * 94 | * @param key 95 | * @param value 96 | */ 97 | public put(key: any, value: any) { 98 | this.entries.set(key, value); 99 | const subject = this.subjects.get(key); 100 | if (subject) { 101 | subject.next(value); 102 | } 103 | } 104 | 105 | /** 106 | * Deletes both the value entry and the corresponding {@link ReplaySubject}. 107 | * Will unsubscribe the {@link ReplaySubject} prior to deleting it, 108 | * severing communication with corresponding {@link Observable}s. 109 | * 110 | * @param key 111 | */ 112 | public delete(key: any) { 113 | //=========================================== 114 | // Delete the entry 115 | //=========================================== 116 | this.entries.delete(key); 117 | const subject = this.subjects.get(key); 118 | if (subject) { 119 | subject.next(undefined); 120 | } 121 | } 122 | 123 | /** 124 | * Clear all entries. 125 | * 126 | * Note that 127 | * this will call delete for on all 128 | * keys defined which also also 129 | * unsubscribes and deletes 130 | * all the sbujects. 131 | */ 132 | public clear() { 133 | for (let key of this.entries.keys()) { 134 | this.delete(key); 135 | } 136 | } 137 | 138 | /** 139 | * Observe changes to the values. 140 | * 141 | * @param key 142 | * @return An {@link Observable} of the value 143 | */ 144 | public observe(key: any): Observable | undefined { 145 | return this.subjects.get(key) 146 | ? this.subjects.get(key)!.asObservable() 147 | : undefined; 148 | } 149 | 150 | /** 151 | * Check whether a value exists. 152 | * 153 | * @param key 154 | * @return True if the entry exists ( Is not null or undefined ) and false otherwise. 155 | */ 156 | public exists(key: any): boolean { 157 | return !!this.entries.get(key); 158 | } 159 | 160 | /** 161 | * Retrieve a snapshot of the 162 | * value. 163 | * 164 | * @param key 165 | * @return A snapshot of the value corresponding to the key. 166 | */ 167 | public snapshot(key: any): any { 168 | return this.entries.get(key); 169 | } 170 | 171 | /** 172 | * Indicates whether the store is empty. 173 | * @return true if the store is empty, false otherwise. 174 | */ 175 | public isEmpty() { 176 | return Array.from(this.entries.values()).length == 0; 177 | } 178 | 179 | /** 180 | * Returns the number of key value pairs contained. 181 | * 182 | * @return the number of entries in the store. 183 | */ 184 | public count() { 185 | return Array.from(this.entries.values()).length; 186 | } 187 | } -------------------------------------------------------------------------------- /projects/slice/src/lib/Slice.spec.ts: -------------------------------------------------------------------------------- 1 | import { Slice } from "./Slice" 2 | import { EStore } from "./EStore" 3 | import { ESTORE_CONFIG_DEFAULT } from "./AbstractStore" 4 | import { TodoSliceEnum } from "./test-setup" 5 | import { Todo, todosFactory } from "./test-setup" 6 | import { attachGUIDs, attachGUID } from './utilities' 7 | import { Observable } from 'rxjs' 8 | 9 | let store = new EStore(todosFactory()) 10 | 11 | let completeSlice = new Slice( 12 | TodoSliceEnum.COMPLETE, 13 | todo => todo.complete, 14 | store 15 | ); 16 | 17 | let incompleteSlice = new Slice( 18 | TodoSliceEnum.INCOMPLETE, 19 | todo => !todo.complete, 20 | store 21 | ); 22 | 23 | describe("Creating a slice", () => { 24 | it("should have 1 incomplete todo element", () => { 25 | expect(incompleteSlice.entries.size).toEqual(1); 26 | }); 27 | it("should have complete todo element", () => { 28 | expect(incompleteSlice.entries.size).toEqual(1); 29 | }); 30 | }); 31 | 32 | describe("Subscribing to a slice", () => { 33 | it("should have 1 incomplete todo element", () => { 34 | let incomplete$ = incompleteSlice.observe(); 35 | incomplete$.subscribe((todos: Todo[]) => { 36 | expect(todos.length).toEqual(1); 37 | expect(todos[0].complete).toBeFalsy(); 38 | }); 39 | }); 40 | it("should have 1 complete todo element", () => { 41 | let complete$ = completeSlice.observe(); 42 | complete$.subscribe((todos: Todo[]) => { 43 | expect(todos.length).toEqual(1); 44 | expect(todos[0].complete).toBeTruthy(); 45 | }); 46 | }); 47 | }); 48 | 49 | describe("Subscribing for slice delta updates", () => { 50 | it("should have 1 incomplete todo element", () => { 51 | let incomplete$ = incompleteSlice.observeDelta(); 52 | incomplete$.subscribe(delta => { 53 | expect(delta.entries.length).toEqual(1); 54 | expect(delta.entries[0].complete).toBeFalsy(); 55 | }); 56 | }); 57 | it("should have 1 complete todo element", () => { 58 | let complete$ = completeSlice.observeDelta(); 59 | complete$.subscribe(delta => { 60 | expect(delta.entries.length).toEqual(1); 61 | expect(delta.entries[0].complete).toBeTruthy(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe("Checking whether the slice is empty", () => { 67 | it("should be empty", () => { 68 | let store = new EStore([]) 69 | 70 | const slice = new Slice(TodoSliceEnum.COMPLETE, todo => todo.complete, store); 71 | slice.isEmpty().subscribe(empty => { 72 | expect(empty).toBeTruthy(); 73 | }); 74 | }); 75 | 76 | it("should not be empty", () => { 77 | let store = new EStore([]) 78 | const slice = new Slice(TodoSliceEnum.COMPLETE, todo => todo.complete, store); 79 | slice.post(new Todo(true, "You completed me!", "1")); 80 | slice.isEmpty().subscribe(empty => { 81 | expect(empty).toBeFalsy(); 82 | }); 83 | }); 84 | }); 85 | 86 | describe("Select slice elements", () => { 87 | it("should select the the right instance by GUID", () => { 88 | let todo = [new Todo(false, "You complete me!")]; 89 | let store = new EStore(todo) 90 | 91 | let id: string = (todo[0])[ESTORE_CONFIG_DEFAULT.guidKey]; 92 | 93 | const slice = new Slice( 94 | TodoSliceEnum.INCOMPLETE, 95 | todo => !todo.complete, 96 | store 97 | ); 98 | 99 | let selectedTodo: Todo = slice.findOne(id)!; 100 | expect(selectedTodo).toEqual(todo[0]); 101 | }); 102 | 103 | it("should select slice element by predicate", () => { 104 | let todo1 = new Todo(false, "You complete me!"); 105 | let todo2 = new Todo(false, "You had me at hello!"); 106 | let todos = [todo1, todo2]; 107 | let store = new EStore(todos) 108 | 109 | const slice = new Slice( 110 | TodoSliceEnum.INCOMPLETE, 111 | todo => !todo.complete, 112 | store 113 | ); 114 | let selectedTodos: Todo[] = slice.select(todo => 115 | todo.title.includes("hello") 116 | ); 117 | expect(selectedTodos[0]).toEqual(todo2); 118 | }); 119 | 120 | it("should select all slice elements", () => { 121 | let todo1 = new Todo(false, "You complete me!"); 122 | let todo2 = new Todo(false, "You had me at hello!"); 123 | let todos = [todo1, todo2]; 124 | let store = new EStore(todos) 125 | 126 | const slice = new Slice( 127 | TodoSliceEnum.INCOMPLETE, 128 | todo => !todo.complete, 129 | store 130 | ); 131 | let selectedTodos: Todo[] = slice.allSnapshot(); 132 | expect(selectedTodos.length).toEqual(2); 133 | }); 134 | 135 | it("should only notify once when adding a todo to a slice observable", () => { 136 | class Todo { 137 | gid!: string; 138 | constructor(public title: string, public completed: boolean) { } 139 | }; 140 | 141 | /** 142 | * The Slice Keys 143 | */ 144 | const enum TodoSliceEnum { 145 | COMPLETE = "Complete", 146 | INCOMPLETE = "Incomplete" 147 | } 148 | 149 | const todoStore: EStore = new EStore(); 150 | 151 | todoStore.addSlice(todo => todo.completed, TodoSliceEnum.COMPLETE); 152 | todoStore.addSlice(todo => !todo.completed, TodoSliceEnum.INCOMPLETE); 153 | 154 | const completeTodos$: Observable = todoStore.getSlice(TodoSliceEnum.COMPLETE)!.observe() 155 | const incompleteTodos$: Observable = todoStore.getSlice(TodoSliceEnum.COMPLETE)!.observe() 156 | 157 | const completeSubscription = completeTodos$.subscribe(todo => console.log(`Completed Todo ${JSON.stringify(todo)})`)) 158 | const incompleteSubscription = incompleteTodos$.subscribe(todo => console.log(`Complete Todo ${JSON.stringify(todo)})}`)) 159 | 160 | const incompleteTodo = new Todo('complete this', false) 161 | const completeTodo = new Todo('You completed me', true) 162 | 163 | todoStore.post(incompleteTodo); 164 | todoStore.post(completeTodo); 165 | 166 | completeSubscription.unsubscribe() 167 | incompleteSubscription.unsubscribe() 168 | }); 169 | }); -------------------------------------------------------------------------------- /projects/slice/src/lib/Slice.ts: -------------------------------------------------------------------------------- 1 | import { Delta, ActionTypes, Predicate } from "./models" 2 | import { AbstractStore } from "./AbstractStore" 3 | import { EStore } from "./EStore"; 4 | import { combineLatest } from 'rxjs'; 5 | 6 | const { isArray } = Array 7 | 8 | export class Slice extends AbstractStore { 9 | 10 | 11 | /* The slice element entries */ 12 | public override entries: Map = new Map(); 13 | 14 | /** 15 | * perform initial notification to all observers, 16 | * such that operations like {@link combineLatest}{} 17 | * will execute at least once. 18 | * 19 | * @param label The slice label 20 | * @param predicate The slice predicate 21 | * @param eStore The EStore instance containing the elements considered for slicing 22 | * 23 | * @example 24 | * ``` 25 | * //Empty slice 26 | * new Slice(Todo.COMPLETE, todo=>!todo.complete); 27 | * 28 | * //Initialized slice 29 | * let todos = [new Todo(false, "You complete me!"), 30 | * new Todo(true, "You completed me!")]; 31 | * new Slice(Todo.COMPLETE, todo=>!todo.complete, todos); 32 | * ``` 33 | */ 34 | constructor( 35 | public label: string, 36 | public predicate: Predicate, 37 | public eStore: EStore) { 38 | super(); 39 | const entities: E[] = eStore.allSnapshot() 40 | this.config = eStore.config 41 | let passed: E[] = this.test(predicate, entities); 42 | const delta: Delta = { type: ActionTypes.INTIALIZE, entries: passed }; 43 | this.post(passed); 44 | this.notifyDelta.next(delta) 45 | } 46 | 47 | /** 48 | * Add the element if it satisfies the predicate 49 | * and notify subscribers that an element was added. 50 | * 51 | * @param e The element to be considered for slicing 52 | */ 53 | post(e: E | E[]) { 54 | if (isArray(e)) { 55 | this.postA(e) 56 | } 57 | else { 58 | if (this.predicate(e)) { 59 | const id = (e)[this.config.guidKey]; 60 | this.entries.set(id, e); 61 | const delta: Delta = { type: ActionTypes.POST, entries: [e] }; 62 | this.notifyAll([...Array.from(this.entries.values())], delta); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Add the elements if they satisfy the predicate 69 | * and notify subscribers that elements were added. 70 | * 71 | * @param e The element to be considered for slicing 72 | */ 73 | postN(...e: E[]) { 74 | this.postA(e); 75 | } 76 | 77 | /** 78 | * Add the elements if they satisfy the predicate 79 | * and notify subscribers that elements were added. 80 | * 81 | * @param e The element to be considered for slicing 82 | */ 83 | postA(e: E[]) { 84 | const d: E[] = []; 85 | e.forEach(e => { 86 | if (this.predicate(e)) { 87 | const id = (e)[this.config.guidKey]; 88 | this.entries.set(id, e); 89 | d.push(e); 90 | } 91 | }); 92 | const delta: Delta = { type: ActionTypes.POST, entries: d }; 93 | this.notifyAll([...Array.from(this.entries.values())], delta); 94 | } 95 | 96 | /** 97 | * Delete an element from the slice. 98 | * 99 | * @param e The element to be deleted if it satisfies the predicate 100 | */ 101 | delete(e: E | E[]) { 102 | if (isArray(e)) { 103 | this.deleteA(e) 104 | } 105 | else { 106 | if (this.predicate(e)) { 107 | const id = (e)[this.config.guidKey] 108 | this.entries.delete(id) 109 | const delta: Delta = { type: ActionTypes.DELETE, entries: [e] } 110 | this.notifyAll(Array.from(this.entries.values()), delta) 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * @param e The elements to be deleted if it satisfies the predicate 117 | */ 118 | deleteN(...e: E[]) { 119 | this.deleteA(e); 120 | } 121 | 122 | /** 123 | * @param e The elements to be deleted if they satisfy the predicate 124 | */ 125 | deleteA(e: E[]) { 126 | const d: E[] = [] 127 | e.forEach(e => { 128 | if (this.predicate(e)) { 129 | const id = (e)[this.config.guidKey] 130 | d.push(this.entries.get(id)!) 131 | this.entries.delete(id) 132 | } 133 | }); 134 | const delta: Delta = { type: ActionTypes.DELETE, entries: d }; 135 | this.notifyAll([...Array.from(this.entries.values())], delta); 136 | } 137 | 138 | /** 139 | * Update the slice when an Entity instance mutates. 140 | * 141 | * @param e The element to be added or deleted depending on predicate reevaluation 142 | */ 143 | put(e: E | E[]) { 144 | if (isArray(e)) { 145 | this.putA(e) 146 | } 147 | else { 148 | const id = (e)[this.config.guidKey]; 149 | if (this.entries.get(id)) { 150 | if (!this.predicate(e)) { 151 | //Note that this is a ActionTypes.DELETE because we are removing the 152 | //entity from the slice. 153 | const delta: Delta = { type: ActionTypes.DELETE, entries: [e] }; 154 | this.entries.delete(id); 155 | this.notifyAll([...Array.from(this.entries.values())], delta); 156 | } 157 | } else if (this.predicate(e)) { 158 | this.entries.set(id, e); 159 | const delta: Delta = { type: ActionTypes.PUT, entries: [e] }; 160 | this.notifyAll([...Array.from(this.entries.values())], delta); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Update the slice with mutated Entity instances. 167 | * 168 | * @param e The elements to be deleted if it satisfies the predicate 169 | */ 170 | putN(...e: E[]) { 171 | this.putA(e); 172 | } 173 | 174 | /** 175 | * @param e The elements to be put 176 | */ 177 | putA(e: E[]) { 178 | const d: E[] = []; //instances to delete 179 | const u: E[] = []; //instances to update 180 | e.forEach(e => { 181 | const id = (e)[this.config.guidKey]; 182 | if (this.entries.get(id)) { 183 | if (!this.predicate(e)) { 184 | d.push(this.entries.get(id)!); 185 | } 186 | } else if (this.predicate(e)) { 187 | u.push(e); 188 | } 189 | }); 190 | if (d.length > 0) { 191 | d.forEach(e => { 192 | this.entries.delete((e)[this.config.guidKey]) 193 | }); 194 | const delta: Delta = { type: ActionTypes.DELETE, entries: d }; 195 | this.notifyAll([...Array.from(this.entries.values())], delta); 196 | } 197 | if (u.length > 0) { 198 | u.forEach(e => { 199 | this.entries.set((e)[this.config.guidKey], e); 200 | }); 201 | const delta: Delta = { type: ActionTypes.PUT, entries: u }; 202 | this.notifyAll([...Array.from(this.entries.values())], delta); 203 | } 204 | } 205 | 206 | /** 207 | * Resets the slice to empty. 208 | */ 209 | reset() { 210 | let delta: Delta = { 211 | type: ActionTypes.RESET, 212 | entries: [...Array.from(this.entries.values())] 213 | }; 214 | this.notifyAll([], delta); 215 | this.entries = new Map(); 216 | } 217 | 218 | /** 219 | * Utility method that applies the predicate to an array 220 | * of entities and return the ones that pass the test. 221 | * 222 | * Used to create an initial set of values 223 | * that should be part of the `Slice`. 224 | * 225 | * @param p 226 | * @param e 227 | * @return The the array of entities that pass the predicate test. 228 | */ 229 | public test(p: Predicate, e: E[]): E[] { 230 | let v: E[] = []; 231 | e.forEach((e: E) => { 232 | if (p(e)) { 233 | v.push(e); 234 | } 235 | }); 236 | return v; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /projects/slice/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models'; 2 | export * from './EStore'; 3 | export * from './OStore'; 4 | export * from './Slice'; 5 | export * from './utilities'; -------------------------------------------------------------------------------- /projects/slice/src/lib/models/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The action types for the store. 3 | */ 4 | export const enum ActionTypes { 5 | POST = "Post", 6 | PUT = "Put", 7 | DELETE = "Delete", 8 | INTIALIZE = "Initialize", 9 | RESET = "Reset" 10 | } 11 | -------------------------------------------------------------------------------- /projects/slice/src/lib/models/Delta.ts: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from "./ActionTypes"; 2 | 3 | /** 4 | * The Delta update interface models 5 | * the type of the update and the entities 6 | * associated with the update. 7 | */ 8 | export interface Delta { 9 | type: ActionTypes; 10 | entries: E[]; 11 | } 12 | -------------------------------------------------------------------------------- /projects/slice/src/lib/models/Entity.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Abstract Entity base class with the 4 | * gid and id properties declared. 5 | */ 6 | export abstract class Entity { 7 | public gid?:string 8 | public id?:string 9 | } -------------------------------------------------------------------------------- /projects/slice/src/lib/models/Predicate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Function type for predicate operations 3 | */ 4 | export type Predicate = (e: E) => boolean; 5 | -------------------------------------------------------------------------------- /projects/slice/src/lib/models/StoreConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The configuration interface for the entity store 3 | * defines the strings for the ID Key and Global ID key. 4 | */ 5 | export interface StoreConfig { 6 | idKey: string; 7 | guidKey: string; 8 | }; 9 | -------------------------------------------------------------------------------- /projects/slice/src/lib/models/constants.ts: -------------------------------------------------------------------------------- 1 | export const SCROLL_UP_DEBOUNCE_TIME_20 = 20; 2 | export const SEARCH_DEBOUNCE_TIME_300 = 300; 3 | -------------------------------------------------------------------------------- /projects/slice/src/lib/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionTypes' 2 | export * from './Delta' 3 | export * from './Predicate' 4 | export * from './StoreConfig' 5 | export * from './Entity' 6 | export * from "./constants" -------------------------------------------------------------------------------- /projects/slice/src/lib/models/scrollPosition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Scroll position function type used to auto hide 3 | * the material toolbar in conjuction with Angular CDK. 4 | */ 5 | export type scrollPosition = ()=>[number, number] -------------------------------------------------------------------------------- /projects/slice/src/lib/test-setup.ts: -------------------------------------------------------------------------------- 1 | export const enum TodoSliceEnum { 2 | COMPLETE = "Complete", 3 | INCOMPLETE = "Incomplete" 4 | } 5 | 6 | export class Todo { 7 | constructor(public complete: boolean, public title: string,public gid?:string, public id?:string) {} 8 | } 9 | 10 | export let todos = [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")]; 11 | 12 | export function todosFactory():Todo[] { 13 | return [new Todo(false, "You complete me!"), new Todo(true, "You completed me!")]; 14 | } 15 | 16 | export function todosClone():Todo[] { 17 | return todos.map(obj => ({...obj})); 18 | } -------------------------------------------------------------------------------- /projects/slice/src/lib/utilities.spec.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { KeyObsValueReset, ObsValueReset, OStore, OStoreStart } from './OStore'; 3 | import { 4 | distinct, 5 | unique, 6 | excludeKeys, 7 | search, 8 | onFilteredEvent, 9 | } from './utilities'; 10 | 11 | type Todo = { 12 | id: any; 13 | title: string; 14 | tags?: string[]; 15 | }; 16 | 17 | it('should create an empty key value store', () => { 18 | let todos: Todo[] = [ 19 | { id: 1, title: 'Lets do it!' }, 20 | { id: 1, title: 'Lets do it again!' }, 21 | { id: 2, title: 'All done!' }, 22 | ]; 23 | 24 | let todos2: Todo[] = [ 25 | { id: 1, title: 'Lets do it!' }, 26 | { id: 2, title: 'All done!' }, 27 | ]; 28 | 29 | expect(distinct(todos, 'id').length).toEqual(2); 30 | expect(unique(todos, 'id')).toBeFalsy(); 31 | expect(distinct(todos2, 'id').length).toEqual(2); 32 | expect(unique(todos2, 'id')).toBeTruthy(); 33 | }); 34 | 35 | it('should exclude keys', () => { 36 | let todo: Todo = { id: 1, title: 'Lets do it!' }; 37 | const keys = excludeKeys(todo, ['id']); 38 | 39 | expect(keys.length).toEqual(1); 40 | expect(keys.includes('title')); 41 | }); 42 | 43 | it('should search the array of todos ', () => { 44 | let todos: Todo[] = [ 45 | { id: 1, title: 'Lets do it!' }, 46 | { id: 1, title: 'Lets do it again!' }, 47 | { id: 2, title: 'All done!' }, 48 | { id: 2, title: 'Tagged todo!', tags: ['t1', 't2'] }, 49 | ]; 50 | 51 | expect(search('again', todos).length).toEqual(1); 52 | expect(search('Lets', todos).length).toEqual(2); 53 | expect(search('All', todos).length).toEqual(1); 54 | expect(search('t1', todos).length).toEqual(1); 55 | expect(search('t2', todos).length).toEqual(1); 56 | // search('t1',todos).forEach(v=>console.log(v)); 57 | }); 58 | 59 | it('should filter events', (done) => { 60 | type NamedEvent = { name: string }; 61 | const namedEvent: NamedEvent = { name: 'hilde' }; 62 | 63 | const START: OStoreStart = { 64 | event: { value: namedEvent }, 65 | }; 66 | interface ISTART extends KeyObsValueReset { 67 | event: ObsValueReset; 68 | } 69 | let OS: OStore = new OStore(START); 70 | 71 | const hildeEvents$: Observable = onFilteredEvent( 72 | 'hilde', 73 | 'name', 74 | OS.S.event.obs! 75 | ); 76 | 77 | OS.put(OS.S.event, { name: 'dagmar' }); 78 | OS.put(OS.S.event, { name: 'hilde', type: 'event' }); 79 | 80 | hildeEvents$.subscribe((e) => { 81 | expect(e['name']).toContain('hilde'); 82 | done(); 83 | }); 84 | }); -------------------------------------------------------------------------------- /projects/slice/src/lib/utilities.ts: -------------------------------------------------------------------------------- 1 | import { ESTORE_CONFIG_DEFAULT } from "./AbstractStore"; 2 | import { Observable, fromEvent, of } from 'rxjs' 3 | import { switchMap, pairwise, debounceTime, distinctUntilChanged, map, filter } from 'rxjs/operators' 4 | import { nanoid} from "nanoid" 5 | import { scrollPosition } from "./models/scrollPosition"; 6 | 7 | /** 8 | * Returns all the entities are distinct by the 9 | * `property` value argument. 10 | * 11 | * Note that the implementation uses a `Map` to 12 | * index the entities by key. Therefore the more recent occurences 13 | * matching a key instance will overwrite the previous ones. 14 | * 15 | * @param property The name of the property to check for distinct values by. 16 | * @param entities The entities in the array. 17 | * 18 | * @example 19 | ``` 20 | let todos: Todo[] = [ 21 | { id: 1, title: "Lets do it!" }, 22 | { id: 1, title: "Lets do it again!" }, 23 | { id: 2, title: "All done!" } 24 | ]; 25 | 26 | let todos2: Todo[] = [ 27 | { id: 1, title: "Lets do it!" }, 28 | { id: 2, title: "All done!" } 29 | ]; 30 | 31 | expect(distinct(todos, "id").length).toEqual(2); 32 | expect(distinct(todos2, "id").length).toEqual(2); 33 | 34 | ``` 35 | */ 36 | export function distinct(entities: E[], property: K): E[] { 37 | const entitiesByProperty = new Map(entities.map(e => [e[property], e] as [E[K], E])); 38 | return Array.from(entitiesByProperty.values()); 39 | } 40 | 41 | /** 42 | * Returns true if all the entities are distinct by the 43 | * `property` value argument. 44 | * 45 | * @param property The name of the property to check for distinct values by. 46 | * @param entities The entities in the array. 47 | * 48 | * @example 49 | * 50 | ``` 51 | let todos: Todo[] = [ 52 | { id: 1, title: "Lets do it!" }, 53 | { id: 1, title: "Lets do it again!" }, 54 | { id: 2, title: "All done!" } 55 | ]; 56 | 57 | let todos2: Todo[] = [ 58 | { id: 1, title: "Lets do it!" }, 59 | { id: 2, title: "All done!" } 60 | ]; 61 | 62 | expect(unique(todos, "id")).toBeFalsy(); 63 | expect(unique(todos2, "id")).toBeTruthy(); 64 | ``` 65 | */ 66 | export function unique(entities: E[], property: keyof E):boolean { 67 | return entities.length == distinct(entities, property).length ? true : false; 68 | } 69 | 70 | /** 71 | * Create a global ID 72 | * @return The global id. 73 | * 74 | * @example 75 | * let e.guid = GUID(); 76 | */ 77 | export function GUID() { 78 | return nanoid(); 79 | } 80 | 81 | /** 82 | * Set the global identfication property on the instance. 83 | * 84 | * @param e Entity we want to set the global identifier on. 85 | * @param gid The name of the `gid` property. If not specified it defaults to `ESTORE_CONFIG_DEFAULT.guidKey`. 86 | */ 87 | export function attachGUID(e: E, gid?: string): string { 88 | const guidKey = gid ? gid : ESTORE_CONFIG_DEFAULT.guidKey 89 | let id: string = nanoid(); 90 | (e)[guidKey] = id 91 | return id 92 | } 93 | 94 | /** 95 | * Set the global identfication property on the instance. 96 | * 97 | * @param e[] Entity array we want to set the global identifiers on. 98 | * @param gid The name of the `gid` property. If not specified it defaults to `gid`. 99 | */ 100 | export function attachGUIDs(e: E[], gid?: string) { 101 | e.forEach(e => { 102 | attachGUID(e, gid); 103 | }); 104 | } 105 | 106 | /** 107 | * Create a shallow copy of the argument. 108 | * @param o The object to copy 109 | */ 110 | export function shallowCopy(o: E) { 111 | return { ...o }; 112 | } 113 | 114 | /** 115 | * Create a deep copy of the argument. 116 | * @param o The object to copy 117 | */ 118 | export function deepCopy(o: E) { 119 | return JSON.parse(JSON.stringify(o)); 120 | } 121 | 122 | /** 123 | * Gets the current active value from the `active` 124 | * Map. 125 | * 126 | * This is used for the scenario where we are managing 127 | * a single active instance. For example 128 | * when selecting a book from a collection of books. 129 | * 130 | * The selected `Book` instance becomes the active value. 131 | * 132 | * @example 133 | * const book:Book = getActiveValue(bookStore.active); 134 | * @param m 135 | */ 136 | export function getActiveValue(m: Map) { 137 | if (m.size) { 138 | return m.entries().next().value[1]; 139 | } 140 | return null; 141 | } 142 | 143 | /** 144 | * The method can be used to exclude keys from an instance 145 | * of type `E`. 146 | * 147 | * We can use this to exclude values when searching an object. 148 | * 149 | * @param entity An instance of type E 150 | * @param exclude The keys to exclude 151 | * 152 | * @example 153 | * todo = { id: '1', description: 'Do it!' } 154 | * let keys = excludeKeys(todo, ['id]); 155 | * // keys = ['description'] 156 | */ 157 | export function excludeKeys(entity: any, exclude: string[]) { 158 | const keys: string[] = Object.keys(entity); 159 | return keys.filter((key) => { 160 | return exclude.indexOf(key) < 0; 161 | }); 162 | } 163 | 164 | /** 165 | * 166 | * @param entities The entity to search 167 | * @param exclude Keys to exclude from each entity 168 | * 169 | * @return E[] Array of entities with properties containing the search term. 170 | */ 171 | export function search(query: string = '', entities: E[], exclude: string[] = []): E[] { 172 | const { isArray } = Array 173 | 174 | query = query.toLowerCase(); 175 | 176 | 177 | return entities.filter(function (e: E) { 178 | //Do the keys calculation on each instance e:E 179 | //because an instance can have optional parameters, 180 | //and thus we have to check each instance, not just 181 | //the first one in the array. 182 | const keys = excludeKeys(e, exclude) 183 | return keys.some( (key) => { 184 | const value = (e as any)[key]; 185 | if (!value) { 186 | return false; 187 | } 188 | if (isArray(value)) { 189 | return value.some(v => { 190 | return String(v).toLowerCase().includes(query); 191 | }); 192 | } 193 | else { 194 | return String(value).toLowerCase().includes(query); 195 | } 196 | }) 197 | }); 198 | } 199 | 200 | /** 201 | * @param scrollable The element being scrolled 202 | * @param debounceMS The number of milliseconds to debounce scroll events 203 | * @param sp The function returning the scroll position coordinates. 204 | * @return A boolean valued observable indicating whether the element is scrolling up or down 205 | */ 206 | export function scrollingUp( 207 | scrollable: any, 208 | debounceMS: number, 209 | sp: scrollPosition): Observable { 210 | return fromEvent(scrollable, 'scroll').pipe( 211 | debounceTime(debounceMS), 212 | distinctUntilChanged(), 213 | map(v => sp()), 214 | pairwise(), 215 | switchMap(p => { 216 | const y1 = p[0][1] 217 | const y2 = p[1][1] 218 | return y1 - y2 > 0 ? of(false) : of(true) 219 | })) 220 | } 221 | 222 | /** 223 | * Filters the entities properties to the set contained in the 224 | * `keys` array. 225 | * 226 | * @param keys The array of keys that the entity be limited to 227 | * @param entity The entity to map 228 | * @return An entity instance that has only the keys provided in the keys array 229 | */ 230 | export function mapEntity(keys:string[], entity:any) { 231 | const result:any = {} 232 | keys.forEach(k=>{ 233 | result[k] = entity[k] 234 | }) 235 | return result 236 | } 237 | 238 | /** 239 | * Returns an `Observable` instance that 240 | * filters for arguments where the property 241 | * value matches the provided value. 242 | * 243 | * @param value The value targeted 244 | * @param propertyName The name of the property to contain the value 245 | * @param obs The Slice Object Store Observable 246 | * @returns Observable 247 | */ 248 | export function onFilteredEvent( 249 | value: any, 250 | propertyName: string, 251 | obs: Observable 252 | ): Observable { 253 | return obs.pipe(filter((e:any) => !!(e && e[propertyName] === value))); 254 | } -------------------------------------------------------------------------------- /projects/slice/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of slice 3 | */ 4 | 5 | export * from './lib/models/' 6 | export * from './lib/AbstractStore' 7 | export * from './lib/EStore' 8 | export * from './lib/OStore' 9 | export * from './lib/Slice' 10 | export * from './lib/utilities' 11 | -------------------------------------------------------------------------------- /projects/slice/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/slice/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/slice/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /slicelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireflysemantics/slice/002192fa596d7b0253eda121750717b3feed4500/slicelogo.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "paths": { 20 | "slice": [ 21 | "./dist/slice" 22 | ] 23 | }, 24 | "target": "ES2022", 25 | "module": "ES2022", 26 | "useDefineForClassFields": false, 27 | "lib": [ 28 | "ES2022", 29 | "dom" 30 | ] 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | --------------------------------------------------------------------------------